From c3fd3cae7acc48015bacab84204131e37cb2f6be Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 19:55:49 -0700 Subject: [PATCH 01/45] core string ops --- packages/bun-types/redis.d.ts | 138 ++++++++++++++++++ src/bun.js/api/valkey.classes.ts | 36 +++++ src/valkey/js_valkey_functions.zig | 11 ++ test/js/valkey/valkey.test.ts | 224 +++++++++++++++++++++++++++++ 4 files changed, 409 insertions(+) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 414b01288b2e..99b831d09c1e 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -245,6 +245,22 @@ declare module "bun" { */ incr(key: RedisClient.KeyLike): Promise; + /** + * Increment the integer value of a key by the given amount + * @param key The key to increment + * @param increment The amount to increment by + * @returns Promise that resolves with the new value after incrementing + */ + incrby(key: RedisClient.KeyLike, increment: number): Promise; + + /** + * Increment the float value of a key by the given amount + * @param key The key to increment + * @param increment The amount to increment by (can be a float) + * @returns Promise that resolves with the new value as a string after incrementing + */ + incrbyfloat(key: RedisClient.KeyLike, increment: number | string): Promise; + /** * Decrement the integer value of a key by one * @param key The key to decrement @@ -252,6 +268,14 @@ declare module "bun" { */ decr(key: RedisClient.KeyLike): Promise; + /** + * Decrement the integer value of a key by the given amount + * @param key The key to decrement + * @param decrement The amount to decrement by + * @returns Promise that resolves with the new value after decrementing + */ + decrby(key: RedisClient.KeyLike, decrement: number): Promise; + /** * Determine if a key exists * @param key The key to check @@ -569,6 +593,36 @@ declare module "bun" { */ setnx(key: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + /** + * Set key to hold the string value with expiration time in seconds + * @param key The key to set + * @param seconds The expiration time in seconds + * @param value The value to set + * @returns Promise that resolves with "OK" on success + * + * @example + * ```ts + * await redis.setex("mykey", 10, "Hello"); + * // Key will expire after 10 seconds + * ``` + */ + setex(key: RedisClient.KeyLike, seconds: number, value: RedisClient.KeyLike): Promise<"OK">; + + /** + * Set key to hold the string value with expiration time in milliseconds + * @param key The key to set + * @param milliseconds The expiration time in milliseconds + * @param value The value to set + * @returns Promise that resolves with "OK" on success + * + * @example + * ```ts + * await redis.psetex("mykey", 10000, "Hello"); + * // Key will expire after 10000 milliseconds (10 seconds) + * ``` + */ + psetex(key: RedisClient.KeyLike, milliseconds: number, value: RedisClient.KeyLike): Promise<"OK">; + /** * Get the score associated with the given member in a sorted set * @param key The sorted set key @@ -586,6 +640,55 @@ declare module "bun" { */ mget(...keys: RedisClient.KeyLike[]): Promise<(string | null)[]>; + /** + * Set multiple keys to multiple values atomically + * + * Sets the given keys to their respective values. MSET replaces existing + * values with new values, just as regular SET. Use MSETNX if you don't want + * to overwrite existing values. + * + * MSET is atomic, so all given keys are set at once. It is not possible for + * clients to see that some of the keys were updated while others are + * unchanged. + * + * @param keyValuePairs Alternating keys and values (key1, value1, key2, value2, ...) + * @returns Promise that resolves with "OK" on success + * + * @example + * ```ts + * await redis.mset("key1", "value1", "key2", "value2"); + * ``` + */ + mset(...keyValuePairs: RedisClient.KeyLike[]): Promise<"OK">; + + /** + * Set multiple keys to multiple values, only if none of the keys exist + * + * Sets the given keys to their respective values. MSETNX will not perform + * any operation at all even if just a single key already exists. + * + * Because of this semantic, MSETNX can be used in order to set different + * keys representing different fields of a unique logic object in a way that + * ensures that either all the fields or none at all are set. + * + * MSETNX is atomic, so all given keys are set at once. It is not possible + * for clients to see that some of the keys were updated while others are + * unchanged. + * + * @param keyValuePairs Alternating keys and values (key1, value1, key2, value2, ...) + * @returns Promise that resolves with 1 if all keys were set, 0 if no key was set + * + * @example + * ```ts + * // Returns 1 if keys don't exist + * await redis.msetnx("key1", "value1", "key2", "value2"); + * + * // Returns 0 if any key already exists + * await redis.msetnx("key1", "newvalue", "key3", "value3"); + * ``` + */ + msetnx(...keyValuePairs: RedisClient.KeyLike[]): Promise; + /** * Count the number of set bits (population counting) in a string * @param key The key to count bits in @@ -593,6 +696,41 @@ declare module "bun" { */ bitcount(key: RedisClient.KeyLike): Promise; + /** + * Returns the bit value at offset in the string value stored at key + * @param key The key containing the string value + * @param offset The bit offset (zero-based) + * @returns Promise that resolves with the bit value (0 or 1) at the specified offset + */ + getbit(key: RedisClient.KeyLike, offset: number): Promise; + + /** + * Sets or clears the bit at offset in the string value stored at key + * @param key The key to modify + * @param offset The bit offset (zero-based) + * @param value The bit value to set (0 or 1) + * @returns Promise that resolves with the original bit value stored at offset + */ + setbit(key: RedisClient.KeyLike, offset: number, value: 0 | 1): Promise; + + /** + * Get a substring of the string stored at a key + * @param key The key to get the substring from + * @param start The starting offset (can be negative to count from the end) + * @param end The ending offset (can be negative to count from the end) + * @returns Promise that resolves with the substring, or an empty string if the key doesn't exist + */ + getrange(key: RedisClient.KeyLike, start: number, end: number): Promise; + + /** + * Overwrite part of a string at key starting at the specified offset + * @param key The key to modify + * @param offset The offset at which to start overwriting (zero-based) + * @param value The string value to write at the offset + * @returns Promise that resolves with the length of the string after modification + */ + setrange(key: RedisClient.KeyLike, offset: number, value: RedisClient.KeyLike): Promise; + /** * Return a serialized version of the value stored at the specified key * @param key The key to dump diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 3828db06d1cb..4c44b9a925f2 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -48,10 +48,22 @@ export default [ fn: "incr", length: 1, }, + incrby: { + fn: "incrby", + length: 2, + }, + incrbyfloat: { + fn: "incrbyfloat", + length: 2, + }, decr: { fn: "decr", length: 1, }, + decrby: { + fn: "decrby", + length: 2, + }, exists: { fn: "exists", length: 1, @@ -123,6 +135,18 @@ export default [ bitcount: { fn: "bitcount", }, + getbit: { + fn: "getbit", + }, + setbit: { + fn: "setbit", + }, + getrange: { + fn: "getrange", + }, + setrange: { + fn: "setrange", + }, dump: { fn: "dump", }, @@ -210,12 +234,24 @@ export default [ setnx: { fn: "setnx", }, + setex: { + fn: "setex", + }, + psetex: { + fn: "psetex", + }, zscore: { fn: "zscore", }, mget: { fn: "mget", }, + mset: { + fn: "mset", + }, + msetnx: { + fn: "msetnx", + }, ping: { fn: "ping" }, publish: { fn: "publish" }, script: { fn: "script" }, diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 06f6befd5194..60345947d08f 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -631,6 +631,10 @@ pub fn ping(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: } pub const bitcount = compile.@"(key: RedisKey)"("bitcount", "BITCOUNT", "key", .not_subscriber).call; +pub const getbit = compile.@"(key: RedisKey, value: RedisValue)"("getbit", "GETBIT", "key", "offset", .not_subscriber).call; +pub const setbit = compile.@"(...strings: string[])"("setbit", "SETBIT", .not_subscriber).call; +pub const getrange = compile.@"(...strings: string[])"("getrange", "GETRANGE", .not_subscriber).call; +pub const setrange = compile.@"(...strings: string[])"("setrange", "SETRANGE", .not_subscriber).call; pub const dump = compile.@"(key: RedisKey)"("dump", "DUMP", "key", .not_subscriber).call; pub const expiretime = compile.@"(key: RedisKey)"("expiretime", "EXPIRETIME", "key", .not_subscriber).call; pub const getdel = compile.@"(key: RedisKey)"("getdel", "GETDEL", "key", .not_subscriber).call; @@ -657,16 +661,23 @@ pub const zrandmember = compile.@"(key: RedisKey)"("zrandmember", "ZRANDMEMBER", pub const append = compile.@"(key: RedisKey, value: RedisValue)"("append", "APPEND", "key", "value", .not_subscriber).call; pub const getset = compile.@"(key: RedisKey, value: RedisValue)"("getset", "GETSET", "key", "value", .not_subscriber).call; pub const hget = compile.@"(key: RedisKey, value: RedisValue)"("hget", "HGET", "key", "field", .not_subscriber).call; +pub const incrby = compile.@"(key: RedisKey, value: RedisValue)"("incrby", "INCRBY", "key", "increment", .not_subscriber).call; +pub const incrbyfloat = compile.@"(key: RedisKey, value: RedisValue)"("incrbyfloat", "INCRBYFLOAT", "key", "increment", .not_subscriber).call; +pub const decrby = compile.@"(key: RedisKey, value: RedisValue)"("decrby", "DECRBY", "key", "decrement", .not_subscriber).call; pub const lpush = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("lpush", "LPUSH", .not_subscriber).call; pub const lpushx = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("lpushx", "LPUSHX", .not_subscriber).call; pub const pfadd = compile.@"(key: RedisKey, value: RedisValue)"("pfadd", "PFADD", "key", "value", .not_subscriber).call; pub const rpush = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("rpush", "RPUSH", .not_subscriber).call; pub const rpushx = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("rpushx", "RPUSHX", .not_subscriber).call; pub const setnx = compile.@"(key: RedisKey, value: RedisValue)"("setnx", "SETNX", "key", "value", .not_subscriber).call; +pub const setex = compile.@"(...strings: string[])"("setex", "SETEX", .not_subscriber).call; +pub const psetex = compile.@"(...strings: string[])"("psetex", "PSETEX", .not_subscriber).call; pub const zscore = compile.@"(key: RedisKey, value: RedisValue)"("zscore", "ZSCORE", "key", "value", .not_subscriber).call; pub const del = compile.@"(key: RedisKey, ...args: RedisKey[])"("del", "DEL", "key", .not_subscriber).call; pub const mget = compile.@"(key: RedisKey, ...args: RedisKey[])"("mget", "MGET", "key", .not_subscriber).call; +pub const mset = compile.@"(...strings: string[])"("mset", "MSET", .not_subscriber).call; +pub const msetnx = compile.@"(...strings: string[])"("msetnx", "MSETNX", .not_subscriber).call; pub const script = compile.@"(...strings: string[])"("script", "SCRIPT", .not_subscriber).call; pub const select = compile.@"(...strings: string[])"("select", "SELECT", .not_subscriber).call; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 1cd2de2fd02a..e77d7f08dee1 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -94,6 +94,102 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(decrementedValue).toBe(10); }); + test("should increment by specified amount with INCRBY", async () => { + const redis = ctx.redis; + const counterKey = "incrby-counter"; + await redis.set(counterKey, "5"); + + // INCRBY should increment by the specified amount + const result1 = await redis.incrby(counterKey, 10); + expect(result1).toBe(15); + + // INCRBY with negative value should decrement + const result2 = await redis.incrby(counterKey, -3); + expect(result2).toBe(12); + + // INCRBY on non-existent key should treat it as 0 + const result3 = await redis.incrby("new-incrby-key", 5); + expect(result3).toBe(5); + }); + + test("should increment by float amount with INCRBYFLOAT", async () => { + const redis = ctx.redis; + const floatKey = "float-counter"; + await redis.set(floatKey, "10.5"); + + // INCRBYFLOAT should increment by the specified float amount + const result1 = await redis.incrbyfloat(floatKey, 2.3); + expect(result1).toBe("12.8"); + + // INCRBYFLOAT with negative value should decrement + const result2 = await redis.incrbyfloat(floatKey, -0.8); + expect(result2).toBe("12"); + + // INCRBYFLOAT on non-existent key should treat it as 0 + const result3 = await redis.incrbyfloat("new-float-key", 3.14); + expect(result3).toBe("3.14"); + }); + + test("should decrement by specified amount with DECRBY", async () => { + const redis = ctx.redis; + const counterKey = "decrby-counter"; + await redis.set(counterKey, "20"); + + // DECRBY should decrement by the specified amount + const result1 = await redis.decrby(counterKey, 5); + expect(result1).toBe(15); + + // DECRBY with larger value + const result2 = await redis.decrby(counterKey, 10); + expect(result2).toBe(5); + + // DECRBY on non-existent key should treat it as 0 + const result3 = await redis.decrby("new-decrby-key", 3); + expect(result3).toBe(-3); + }); + + test("should set multiple keys with MSET", async () => { + const redis = ctx.redis; + + // MSET should set multiple keys atomically + const result = await redis.mset("mset-key1", "value1", "mset-key2", "value2", "mset-key3", "value3"); + expect(result).toBe("OK"); + + // Verify all keys were set + const value1 = await redis.get("mset-key1"); + expect(value1).toBe("value1"); + const value2 = await redis.get("mset-key2"); + expect(value2).toBe("value2"); + const value3 = await redis.get("mset-key3"); + expect(value3).toBe("value3"); + }); + + test("should set multiple keys only if none exist with MSETNX", async () => { + const redis = ctx.redis; + + // First MSETNX should succeed (keys don't exist) + const result1 = await redis.msetnx("msetnx-key1", "value1", "msetnx-key2", "value2"); + expect(result1).toBe(1); + + // Verify keys were set + const value1 = await redis.get("msetnx-key1"); + expect(value1).toBe("value1"); + const value2 = await redis.get("msetnx-key2"); + expect(value2).toBe("value2"); + + // Second MSETNX should fail (at least one key exists) + const result2 = await redis.msetnx("msetnx-key1", "newvalue", "msetnx-key3", "value3"); + expect(result2).toBe(0); + + // Verify original values weren't changed + const unchangedValue = await redis.get("msetnx-key1"); + expect(unchangedValue).toBe("value1"); + + // And new key wasn't created + const nonExistentKey = await redis.get("msetnx-key3"); + expect(nonExistentKey).toBeNull(); + }); + test("should manage key expiration", async () => { const redis = ctx.redis; // Set a key first @@ -112,6 +208,134 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(ttl).toBeLessThanOrEqual(60); // Should be positive and not exceed our set time }); + test("should set key with expiration using SETEX", async () => { + const redis = ctx.redis; + const key = "setex-test-key"; + const value = "test-value"; + + // SETEX should set the key with expiration in seconds + const result = await redis.setex(key, 10, value); + expect(result).toBe("OK"); + + // Verify the value was set + const getValue = await redis.get(key); + expect(getValue).toBe(value); + + // Verify TTL is set (should be <= 10 seconds) + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(10); + }); + + test("should set key with expiration using PSETEX", async () => { + const redis = ctx.redis; + const key = "psetex-test-key"; + const value = "test-value"; + + // PSETEX should set the key with expiration in milliseconds + const result = await redis.psetex(key, 5000, value); + expect(result).toBe("OK"); + + // Verify the value was set + const getValue = await redis.get(key); + expect(getValue).toBe(value); + + // Verify TTL is set (should be <= 5000 milliseconds, i.e., <= 5 seconds) + const pttl = await redis.pttl(key); + expect(pttl).toBeGreaterThan(0); + expect(pttl).toBeLessThanOrEqual(5000); + }); + + test("should get and set bits", async () => { + const redis = ctx.redis; + const bitKey = "mybitkey"; + + // Set a bit at offset 7 to 1 + const oldValue = await redis.setbit(bitKey, 7, 1); + expect(oldValue).toBe(0); // Original value was 0 + + // Get the bit at offset 7 + const bitValue = await redis.getbit(bitKey, 7); + expect(bitValue).toBe(1); + + // Get a bit that wasn't set (should be 0) + const unsetBit = await redis.getbit(bitKey, 100); + expect(unsetBit).toBe(0); + + // Set the same bit again to 0 + const oldValue2 = await redis.setbit(bitKey, 7, 0); + expect(oldValue2).toBe(1); // Previous value was 1 + + // Verify it's now 0 + const bitValue2 = await redis.getbit(bitKey, 7); + expect(bitValue2).toBe(0); + }); + + test("should handle multiple bit operations", async () => { + const redis = ctx.redis; + const bitKey = "multibit"; + + // Set multiple bits + await redis.setbit(bitKey, 0, 1); + await redis.setbit(bitKey, 3, 1); + await redis.setbit(bitKey, 7, 1); + + // Verify all bits + expect(await redis.getbit(bitKey, 0)).toBe(1); + expect(await redis.getbit(bitKey, 1)).toBe(0); + expect(await redis.getbit(bitKey, 2)).toBe(0); + expect(await redis.getbit(bitKey, 3)).toBe(1); + expect(await redis.getbit(bitKey, 4)).toBe(0); + expect(await redis.getbit(bitKey, 5)).toBe(0); + expect(await redis.getbit(bitKey, 6)).toBe(0); + expect(await redis.getbit(bitKey, 7)).toBe(1); + + // Count the set bits + const count = await redis.bitcount(bitKey); + expect(count).toBe(3); + }); + + test("should get range of string", async () => { + const redis = ctx.redis; + const key = "rangetest"; + await redis.set(key, "Hello World"); + + // Get substring from start to end + const result1 = await redis.getrange(key, 0, 4); + expect(result1).toBe("Hello"); + + // Get substring with different range + const result2 = await redis.getrange(key, 6, 10); + expect(result2).toBe("World"); + + // Get with negative offsets (count from end) + const result3 = await redis.getrange(key, -5, -1); + expect(result3).toBe("World"); + + // Get entire string + const result4 = await redis.getrange(key, 0, -1); + expect(result4).toBe("Hello World"); + }); + + test("should set range of string", async () => { + const redis = ctx.redis; + const key = "setrangetest"; + await redis.set(key, "Hello World"); + + // Overwrite part of the string + const newLength = await redis.setrange(key, 6, "Redis"); + expect(newLength).toBe(11); + + // Verify the change + const result = await redis.get(key); + expect(result).toBe("Hello Redis"); + + // Set range on non-existent key (should pad with zero bytes) + const key2 = "newkey"; + const newLength2 = await redis.setrange(key2, 5, "Redis"); + expect(newLength2).toBeGreaterThanOrEqual(10); + }); + test("should implement TTL command correctly for different cases", async () => { const redis = ctx.redis; // 1. Key with expiration From 9a56a309a12b50730df90245b2b6a4cc411ca444 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 20:26:43 -0700 Subject: [PATCH 02/45] feat: Implement key management commands --- packages/bun-types/redis.d.ts | 275 +++++++++++++++++- src/bun.js/api/valkey.classes.ts | 14 + src/valkey/js_valkey.zig | 18 ++ src/valkey/js_valkey_functions.zig | 31 +++ test/js/valkey/valkey.test.ts | 430 ++++++++++++++++++++++++++++- 5 files changed, 758 insertions(+), 10 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 99b831d09c1e..2592908c0b77 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -34,14 +34,7 @@ declare module "bun" { * TLS options * Can be a boolean or an object with TLS options */ - tls?: - | boolean - | { - key?: string | Buffer; - cert?: string | Buffer; - ca?: string | Buffer | Array; - rejectUnauthorized?: boolean; - }; + tls?: boolean | Bun.TLSOptions; /** * Whether to enable auto-pipelining @@ -466,6 +459,25 @@ declare module "bun" { */ pttl(key: RedisClient.KeyLike): Promise; + /** + * Return a random key from the keyspace + * + * Returns a random key from the currently selected database. + * + * @returns Promise that resolves with a random key name, or null if the + * database is empty + * + * @example + * ```ts + * await redis.set("key1", "value1"); + * await redis.set("key2", "value2"); + * await redis.set("key3", "value3"); + * const randomKey = await redis.randomkey(); + * console.log(randomKey); // One of: "key1", "key2", or "key3" + * ``` + */ + randomkey(): Promise; + /** * Remove and get the last element in a list * @param key The list key @@ -473,6 +485,98 @@ declare module "bun" { */ rpop(key: RedisClient.KeyLike): Promise; + /** + * Incrementally iterate the keyspace + * + * The SCAN command is used to incrementally iterate over a collection of + * elements. SCAN iterates the set of keys in the currently selected Redis + * database. + * + * SCAN is a cursor based iterator. This means that at every call of the + * command, the server returns an updated cursor that the user needs to use + * as the cursor argument in the next call. + * + * An iteration starts when the cursor is set to "0", and terminates when + * the cursor returned by the server is "0". + * + * @param cursor The cursor value (use "0" to start a new iteration) + * @returns Promise that resolves with a tuple [cursor, keys[]] where cursor + * is the next cursor to use (or "0" if iteration is complete) and keys is + * an array of matching keys + * + * @example + * ```ts + * // Basic scan - iterate all keys + * let cursor = "0"; + * const allKeys: string[] = []; + * do { + * const [nextCursor, keys] = await redis.scan(cursor); + * allKeys.push(...keys); + * cursor = nextCursor; + * } while (cursor !== "0"); + * ``` + * + * @example + * ```ts + * // Scan with MATCH pattern + * const [cursor, keys] = await redis.scan("0", "MATCH", "user:*"); + * ``` + * + * @example + * ```ts + * // Scan with COUNT hint + * const [cursor, keys] = await redis.scan("0", "COUNT", "100"); + * ``` + */ + scan(cursor: string | number): Promise<[string, string[]]>; + + /** + * Incrementally iterate the keyspace with a pattern match + * + * @param cursor The cursor value (use "0" to start a new iteration) + * @param match The "MATCH" keyword + * @param pattern The pattern to match (supports glob-style patterns like "user:*") + * @returns Promise that resolves with a tuple [cursor, keys[]] + */ + scan(cursor: string | number, match: "MATCH", pattern: string): Promise<[string, string[]]>; + + /** + * Incrementally iterate the keyspace with a count hint + * + * @param cursor The cursor value (use "0" to start a new iteration) + * @param count The "COUNT" keyword + * @param hint The number of elements to return per call (hint only, not exact) + * @returns Promise that resolves with a tuple [cursor, keys[]] + */ + scan(cursor: string | number, count: "COUNT", hint: number): Promise<[string, string[]]>; + + /** + * Incrementally iterate the keyspace with pattern match and count hint + * + * @param cursor The cursor value (use "0" to start a new iteration) + * @param match The "MATCH" keyword + * @param pattern The pattern to match + * @param count The "COUNT" keyword + * @param hint The number of elements to return per call + * @returns Promise that resolves with a tuple [cursor, keys[]] + */ + scan( + cursor: string | number, + match: "MATCH", + pattern: string, + count: "COUNT", + hint: number, + ): Promise<[string, string[]]>; + + /** + * Incrementally iterate the keyspace with options + * + * @param cursor The cursor value + * @param options Additional SCAN options (MATCH pattern, COUNT hint, etc.) + * @returns Promise that resolves with a tuple [cursor, keys[]] + */ + scan(cursor: string | number, ...options: (string | number)[]): Promise<[string, string[]]>; + /** * Get the number of members in a set * @param key The set key @@ -489,6 +593,36 @@ declare module "bun" { */ strlen(key: RedisClient.KeyLike): Promise; + /** + * Determine the type of value stored at key + * + * The TYPE command returns the string representation of the type of the + * value stored at key. The different types that can be returned are: + * string, list, set, zset, hash and stream. + * + * @param key The key to check + * @returns Promise that resolves with the type of value stored at key, or + * "none" if the key doesn't exist + * + * @example + * ```ts + * await redis.set("mykey", "Hello"); + * console.log(await redis.type("mykey")); // "string" + * + * await redis.lpush("mylist", "value"); + * console.log(await redis.type("mylist")); // "list" + * + * await redis.sadd("myset", "value"); + * console.log(await redis.type("myset")); // "set" + * + * await redis.hset("myhash", "field", "value"); + * console.log(await redis.type("myhash")); // "hash" + * + * console.log(await redis.type("nonexistent")); // "none" + * ``` + */ + type(key: RedisClient.KeyLike): Promise<"none" | "string" | "list" | "set" | "zset" | "hash" | "stream">; + /** * Get the number of members in a sorted set * @param key The sorted set key @@ -950,6 +1084,131 @@ declare module "bun" { * This will open up a new connection to the Redis server. */ duplicate(): Promise; + + /** + * Copy the value stored at the source key to the destination key + * + * By default, the destination key is created in the logical database used + * by the connection. The REPLACE option removes the destination key before + * copying the value to it. + * + * @param source The source key to copy from + * @param destination The destination key to copy to + * @returns Promise that resolves with 1 if the key was copied, 0 if not + * + * @example + * ```ts + * await redis.set("mykey", "Hello"); + * await redis.copy("mykey", "myotherkey"); + * console.log(await redis.get("myotherkey")); // "Hello" + * ``` + */ + copy(source: RedisClient.KeyLike, destination: RedisClient.KeyLike): Promise; + + /** + * Copy the value stored at the source key to the destination key, optionally replacing it + * + * The REPLACE option removes the destination key before copying the value to it. + * + * @param source The source key to copy from + * @param destination The destination key to copy to + * @param replace "REPLACE" - Remove the destination key before copying + * @returns Promise that resolves with 1 if the key was copied, 0 if not + * + * @example + * ```ts + * await redis.set("mykey", "Hello"); + * await redis.set("myotherkey", "World"); + * await redis.copy("mykey", "myotherkey", "REPLACE"); + * console.log(await redis.get("myotherkey")); // "Hello" + * ``` + */ + copy(source: RedisClient.KeyLike, destination: RedisClient.KeyLike, replace: "REPLACE"): Promise; + + /** + * Asynchronously delete one or more keys + * + * This command is very similar to DEL: it removes the specified keys. + * Just like DEL a key is ignored if it does not exist. However, the + * command performs the actual memory reclaiming in a different thread, so + * it is not blocking, while DEL is. This is particularly useful when + * deleting large values or large numbers of keys. + * + * @param keys The keys to delete + * @returns Promise that resolves with the number of keys that were unlinked + * + * @example + * ```ts + * await redis.set("key1", "Hello"); + * await redis.set("key2", "World"); + * const count = await redis.unlink("key1", "key2", "key3"); + * console.log(count); // 2 + * ``` + */ + unlink(...keys: RedisClient.KeyLike[]): Promise; + + /** + * Alters the last access time of one or more keys + * + * A key is ignored if it does not exist. The command returns the number + * of keys that were touched. + * + * This command is useful in conjunction with maxmemory-policy + * allkeys-lru / volatile-lru to change the last access time of keys for + * eviction purposes. + * + * @param keys One or more keys to touch + * @returns Promise that resolves with the number of keys that were touched + * + * @example + * ```ts + * await redis.set("key1", "Hello"); + * await redis.set("key2", "World"); + * const touched = await redis.touch("key1", "key2", "key3"); + * console.log(touched); // 2 (key3 doesn't exist) + * ``` + */ + touch(...keys: RedisClient.KeyLike[]): Promise; + + /** + * Rename a key to a new key + * + * Renames key to newkey. If newkey already exists, it is overwritten. If + * key does not exist, an error is returned. + * + * @param key The key to rename + * @param newkey The new key name + * @returns Promise that resolves with "OK" on success + * + * @example + * ```ts + * await redis.set("mykey", "Hello"); + * await redis.rename("mykey", "myotherkey"); + * const value = await redis.get("myotherkey"); // "Hello" + * const oldValue = await redis.get("mykey"); // null + * ``` + */ + rename(key: RedisClient.KeyLike, newkey: RedisClient.KeyLike): Promise<"OK">; + + /** + * Rename a key to a new key only if the new key does not exist + * + * Renames key to newkey only if newkey does not yet exist. If key does not + * exist, an error is returned. + * + * @param key The key to rename + * @param newkey The new key name + * @returns Promise that resolves with 1 if the key was renamed, 0 if newkey already exists + * + * @example + * ```ts + * await redis.set("mykey", "Hello"); + * await redis.renamenx("mykey", "myotherkey"); // Returns 1 + * await redis.set("mykey2", "World"); + * await redis.renamenx("mykey2", "myotherkey"); // Returns 0 (myotherkey exists) + * ``` + */ + renamenx(key: RedisClient.KeyLike, newkey: RedisClient.KeyLike): Promise; } /** diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 4c44b9a925f2..7b4c6682f0c0 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -189,15 +189,24 @@ export default [ pttl: { fn: "pttl", }, + randomkey: { + fn: "randomkey", + }, rpop: { fn: "rpop", }, + scan: { + fn: "scan", + }, scard: { fn: "scard", }, strlen: { fn: "strlen", }, + type: { + fn: "type", + }, zcard: { fn: "zcard", }, @@ -268,6 +277,11 @@ export default [ unsubscribe: { fn: "unsubscribe" }, punsubscribe: { fn: "punsubscribe" }, pubsub: { fn: "pubsub" }, + copy: { fn: "copy" }, + unlink: { fn: "unlink" }, + touch: { fn: "touch" }, + rename: { fn: "rename" }, + renamenx: { fn: "renamenx" }, }, values: ["onconnect", "onclose", "connectionPromise", "hello", "subscriptionCallbackMap"], }), diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 31342eaa1b45..3fbd28e44cb3 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1243,7 +1243,9 @@ pub const JSValkeyClient = struct { pub const @"type" = fns.type; pub const append = fns.append; pub const bitcount = fns.bitcount; + pub const copy = fns.copy; pub const decr = fns.decr; + pub const decrby = fns.decrby; pub const del = fns.del; pub const dump = fns.dump; pub const duplicate = fns.duplicate; @@ -1252,8 +1254,10 @@ pub const JSValkeyClient = struct { pub const expiretime = fns.expiretime; pub const get = fns.get; pub const getBuffer = fns.getBuffer; + pub const getbit = fns.getbit; pub const getdel = fns.getdel; pub const getex = fns.getex; + pub const getrange = fns.getrange; pub const getset = fns.getset; pub const hgetall = fns.hgetall; pub const hget = fns.hget; @@ -1266,30 +1270,42 @@ pub const JSValkeyClient = struct { pub const hstrlen = fns.hstrlen; pub const hvals = fns.hvals; pub const incr = fns.incr; + pub const incrby = fns.incrby; + pub const incrbyfloat = fns.incrbyfloat; pub const keys = fns.keys; pub const llen = fns.llen; pub const lpop = fns.lpop; pub const lpush = fns.lpush; pub const lpushx = fns.lpushx; pub const mget = fns.mget; + pub const mset = fns.mset; + pub const msetnx = fns.msetnx; pub const persist = fns.persist; pub const pexpiretime = fns.pexpiretime; pub const pfadd = fns.pfadd; pub const ping = fns.ping; + pub const psetex = fns.psetex; pub const psubscribe = fns.psubscribe; pub const pttl = fns.pttl; pub const publish = fns.publish; pub const pubsub = fns.pubsub; pub const punsubscribe = fns.punsubscribe; + pub const randomkey = fns.randomkey; + pub const rename = fns.rename; + pub const renamenx = fns.renamenx; pub const rpop = fns.rpop; pub const rpush = fns.rpush; pub const rpushx = fns.rpushx; pub const sadd = fns.sadd; + pub const scan = fns.scan; pub const scard = fns.scard; pub const script = fns.script; pub const select = fns.select; pub const set = fns.set; + pub const setbit = fns.setbit; + pub const setex = fns.setex; pub const setnx = fns.setnx; + pub const setrange = fns.setrange; pub const sismember = fns.sismember; pub const smembers = fns.smembers; pub const smove = fns.smove; @@ -1300,7 +1316,9 @@ pub const JSValkeyClient = struct { pub const strlen = fns.strlen; pub const subscribe = fns.subscribe; pub const substr = fns.substr; + pub const touch = fns.touch; pub const ttl = fns.ttl; + pub const unlink = fns.unlink; pub const unsubscribe = fns.unsubscribe; pub const zcard = fns.zcard; pub const zpopmax = fns.zpopmax; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 60345947d08f..0c5241547420 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -649,7 +649,9 @@ pub const lpop = compile.@"(key: RedisKey)"("lpop", "LPOP", "key", .not_subscrib pub const persist = compile.@"(key: RedisKey)"("persist", "PERSIST", "key", .not_subscriber).call; pub const pexpiretime = compile.@"(key: RedisKey)"("pexpiretime", "PEXPIRETIME", "key", .not_subscriber).call; pub const pttl = compile.@"(key: RedisKey)"("pttl", "PTTL", "key", .not_subscriber).call; +pub const randomkey = compile.@"()"("randomkey", "RANDOMKEY", .not_subscriber).call; pub const rpop = compile.@"(key: RedisKey)"("rpop", "RPOP", "key", .not_subscriber).call; +pub const scan = compile.@"(...strings: string[])"("scan", "SCAN", .not_subscriber).call; pub const scard = compile.@"(key: RedisKey)"("scard", "SCARD", "key", .not_subscriber).call; pub const strlen = compile.@"(key: RedisKey)"("strlen", "STRLEN", "key", .not_subscriber).call; pub const @"type" = compile.@"(key: RedisKey)"("type", "TYPE", "key", .not_subscriber).call; @@ -690,6 +692,11 @@ pub const zrevrank = compile.@"(...strings: string[])"("zrevrank", "ZREVRANK", . pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE", .dont_care).call; pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE", .dont_care).call; pub const pubsub = compile.@"(...strings: string[])"("pubsub", "PUBSUB", .dont_care).call; +pub const copy = compile.@"(...strings: string[])"("copy", "COPY", .not_subscriber).call; +pub const unlink = compile.@"(key: RedisKey, ...args: RedisKey[])"("unlink", "UNLINK", "key", .not_subscriber).call; +pub const touch = compile.@"(key: RedisKey, ...args: RedisKey[])"("touch", "TOUCH", "key", .not_subscriber).call; +pub const rename = compile.@"(key: RedisKey, value: RedisValue)"("rename", "RENAME", "key", "newkey", .not_subscriber).call; +pub const renamenx = compile.@"(key: RedisKey, value: RedisValue)"("renamenx", "RENAMENX", "key", "newkey", .not_subscriber).call; pub fn publish( this: *JSValkeyClient, @@ -1015,6 +1022,30 @@ const compile = struct { }; } + pub fn @"()"( + comptime name: []const u8, + comptime command: []const u8, + comptime client_state_requirement: ClientStateRequirement, + ) type { + return struct { + pub fn call(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + try testCorrectState(this, name, client_state_requirement); + + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = command, + .args = .{ .args = &.{} }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send " ++ command, err); + }; + return promise.toJS(); + } + }; + } + pub fn @"(key: RedisKey)"( comptime name: []const u8, comptime command: []const u8, diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index e77d7f08dee1..f6eb866d98b6 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -1,5 +1,6 @@ import { randomUUIDv7, RedisClient, spawn } from "bun"; import { beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { bunExe } from "harness"; import { ctx as _ctx, awaitableCounter, @@ -16,7 +17,7 @@ import { import type { RedisTestStartMessage } from "./valkey.failing-subscriber"; for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { - const ctx = { ..._ctx, redis: connectionType ? _ctx.redis : _ctx.redisTLS }; + const ctx = { ..._ctx, redis: connectionType ? _ctx.redis : (_ctx.redisTLS as RedisClient) }; describe.skipIf(!isEnabled)(`Valkey Redis Client (${connectionType})`, () => { beforeAll(async () => { // Ensure container is ready before tests run @@ -148,6 +149,93 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(result3).toBe(-3); }); + test("should rename a key with RENAME", async () => { + const redis = ctx.redis; + const oldKey = "old-key"; + const newKey = "new-key"; + const value = "test-value"; + + // Set a value on the old key + await redis.set(oldKey, value); + + // Rename the key + const result = await redis.rename(oldKey, newKey); + expect(result).toBe("OK"); + + // The new key should have the value + const newValue = await redis.get(newKey); + expect(newValue).toBe(value); + + // The old key should no longer exist + const oldValue = await redis.get(oldKey); + expect(oldValue).toBeNull(); + }); + + test("should rename a key with RENAME overwriting existing key", async () => { + const redis = ctx.redis; + const oldKey = "old-key-overwrite"; + const newKey = "new-key-overwrite"; + + // Set values on both keys + await redis.set(oldKey, "old-value"); + await redis.set(newKey, "existing-value"); + + // Rename should overwrite the existing key + const result = await redis.rename(oldKey, newKey); + expect(result).toBe("OK"); + + // The new key should have the old value + const newValue = await redis.get(newKey); + expect(newValue).toBe("old-value"); + + // The old key should no longer exist + const oldValue = await redis.get(oldKey); + expect(oldValue).toBeNull(); + }); + + test("should rename a key only if new key does not exist with RENAMENX", async () => { + const redis = ctx.redis; + const oldKey = "old-key-nx"; + const newKey = "new-key-nx"; + const value = "test-value"; + + // Set a value on the old key + await redis.set(oldKey, value); + + // RENAMENX should succeed (newkey doesn't exist) + const result1 = await redis.renamenx(oldKey, newKey); + expect(result1).toBe(1); + + // The new key should have the value + const newValue = await redis.get(newKey); + expect(newValue).toBe(value); + + // The old key should no longer exist + const oldValue = await redis.get(oldKey); + expect(oldValue).toBeNull(); + }); + + test("should not rename if new key exists with RENAMENX", async () => { + const redis = ctx.redis; + const oldKey = "old-key-nx-fail"; + const newKey = "new-key-nx-fail"; + + // Set values on both keys + await redis.set(oldKey, "old-value"); + await redis.set(newKey, "existing-value"); + + // RENAMENX should fail (newkey exists) + const result = await redis.renamenx(oldKey, newKey); + expect(result).toBe(0); + + // Both keys should retain their original values + const oldValue = await redis.get(oldKey); + expect(oldValue).toBe("old-value"); + + const newValue = await redis.get(newKey); + expect(newValue).toBe("existing-value"); + }); + test("should set multiple keys with MSET", async () => { const redis = ctx.redis; @@ -246,6 +334,54 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(pttl).toBeLessThanOrEqual(5000); }); + test("should determine the type of a key with TYPE", async () => { + const redis = ctx.redis; + + // String type + await redis.set("string-key", "value"); + const stringType = await redis.type("string-key"); + expect(stringType).toBe("string"); + + // List type + await redis.lpush("list-key", "value"); + const listType = await redis.type("list-key"); + expect(listType).toBe("list"); + + // Set type + await redis.sadd("set-key", "value"); + const setType = await redis.type("set-key"); + expect(setType).toBe("set"); + + // Hash type + await redis.send("HSET", ["hash-key", "field", "value"]); + const hashType = await redis.type("hash-key"); + expect(hashType).toBe("hash"); + + // Non-existent key + const noneType = await redis.type("nonexistent-key"); + expect(noneType).toBe("none"); + }); + + test("should update last access time with TOUCH", async () => { + const redis = ctx.redis; + + // Set some keys + await redis.set("touch-key1", "value1"); + await redis.set("touch-key2", "value2"); + + // Touch existing keys + const touchedCount = await redis.touch("touch-key1", "touch-key2"); + expect(touchedCount).toBe(2); + + // Touch mix of existing and non-existing keys + const mixedCount = await redis.touch("touch-key1", "nonexistent-key"); + expect(mixedCount).toBe(1); + + // Touch non-existent key + const noneCount = await redis.touch("nonexistent-key1", "nonexistent-key2"); + expect(noneCount).toBe(0); + }); + test("should get and set bits", async () => { const redis = ctx.redis; const bitKey = "mybitkey"; @@ -360,6 +496,296 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const noKey = await redis.ttl(nonExistentKey); expect(noKey).toMatchInlineSnapshot(`-2`); // -2 indicates key doesn't exist }); + + test("should copy a key to a new key with COPY", async () => { + const redis = ctx.redis; + const sourceKey = "copy-source"; + const destKey = "copy-dest"; + + // Set source key + await redis.set(sourceKey, "Hello World"); + + // Copy to destination + const result = await redis.copy(sourceKey, destKey); + expect(result).toBe(1); // 1 indicates successful copy + + // Verify both keys exist with same value + const sourceValue = await redis.get(sourceKey); + const destValue = await redis.get(destKey); + expect(sourceValue).toBe("Hello World"); + expect(destValue).toBe("Hello World"); + + // Trying to copy to an existing key without REPLACE should fail + const result2 = await redis.copy(sourceKey, destKey); + expect(result2).toBe(0); // 0 indicates copy failed + }); + + test("should copy a key with REPLACE option", async () => { + const redis = ctx.redis; + const sourceKey = "copy-replace-source"; + const destKey = "copy-replace-dest"; + + // Set both keys + await redis.set(sourceKey, "New Value"); + await redis.set(destKey, "Old Value"); + + // Copy with REPLACE + const result = await redis.copy(sourceKey, destKey, "REPLACE"); + expect(result).toBe(1); + + // Verify destination was replaced + const destValue = await redis.get(destKey); + expect(destValue).toBe("New Value"); + }); + + test("should unlink one or more keys asynchronously with UNLINK", async () => { + const redis = ctx.redis; + + // Set multiple keys + await redis.set("unlink-key1", "value1"); + await redis.set("unlink-key2", "value2"); + await redis.set("unlink-key3", "value3"); + + // Unlink multiple keys + const result = await redis.unlink("unlink-key1", "unlink-key2", "unlink-key3"); + expect(result).toBe(3); // All 3 keys were unlinked + + // Verify keys are gone + expect(await redis.get("unlink-key1")).toBeNull(); + expect(await redis.get("unlink-key2")).toBeNull(); + expect(await redis.get("unlink-key3")).toBeNull(); + }); + + test("should unlink with non-existent keys", async () => { + const redis = ctx.redis; + + // Set one key + await redis.set("unlink-exists", "value"); + + // Try to unlink mix of existing and non-existing keys + const result = await redis.unlink("unlink-exists", "unlink-nonexist1", "unlink-nonexist2"); + expect(result).toBe(1); // Only 1 key existed and was unlinked + + // Verify key is gone + expect(await redis.get("unlink-exists")).toBeNull(); + }); + + test("should return a random key with RANDOMKEY", async () => { + const redis = ctx.redis; + + // Empty database should return null + const emptyResult = await redis.randomkey(); + expect(emptyResult).toBeNull(); + + // Set multiple keys + await redis.set("random-key1", "value1"); + await redis.set("random-key2", "value2"); + await redis.set("random-key3", "value3"); + + // Get a random key + const randomKey = await redis.randomkey(); + expect(randomKey).toBeDefined(); + expect(randomKey).not.toBeNull(); + expect(["random-key1", "random-key2", "random-key3"]).toContain(randomKey); + + // Verify the key exists + const value = await redis.get(randomKey!); + expect(value).toBeDefined(); + }); + + test("should iterate keys with SCAN", async () => { + const redis = ctx.redis; + + // Set multiple keys with a pattern + const testKeys = ["scan-test:1", "scan-test:2", "scan-test:3", "scan-test:4", "scan-test:5"]; + for (const key of testKeys) { + await redis.set(key, "value"); + } + + // Scan all keys + let cursor = "0"; + const foundKeys: string[] = []; + do { + const [nextCursor, keys] = await redis.scan(cursor); + foundKeys.push(...keys); + cursor = nextCursor; + } while (cursor !== "0"); + + // Verify all test keys were found + for (const testKey of testKeys) { + expect(foundKeys).toContain(testKey); + } + }); + + test("should iterate keys with SCAN and MATCH pattern", async () => { + const redis = ctx.redis; + + // Set keys with different patterns + await redis.set("user:1", "alice"); + await redis.set("user:2", "bob"); + await redis.set("post:1", "hello"); + await redis.set("post:2", "world"); + + // Scan with MATCH pattern + let cursor = "0"; + const userKeys: string[] = []; + do { + const [nextCursor, keys] = await redis.scan(cursor, "MATCH", "user:*"); + userKeys.push(...keys); + cursor = nextCursor; + } while (cursor !== "0"); + + // Should only find user keys + expect(userKeys).toContain("user:1"); + expect(userKeys).toContain("user:2"); + expect(userKeys).not.toContain("post:1"); + expect(userKeys).not.toContain("post:2"); + }); + + test("should reject invalid object argument in SCAN", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.scan({} as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'scan'."`); + }); + + test("should reject invalid array argument in SCAN", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.scan([] as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'scan'."`); + }); + + test("should reject invalid null argument in SCAN", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.scan(null as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'scan'."`); + }); + + test("should reject invalid source key in COPY", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.copy({} as any, "dest"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'copy'."`); + }); + + test("should reject invalid destination key in COPY", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.copy("source", [] as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'copy'."`); + }); + + test("should reject invalid option in COPY", async () => { + const redis = ctx.redis; + await redis.set("copy-invalid-opt-source", "value"); + expect(async () => { + await redis.copy("copy-invalid-opt-source", "copy-invalid-opt-dest", "NOTVALID" as any); + }).toThrowErrorMatchingInlineSnapshot(`"ERR syntax error"`); + }); + + test("should reject invalid old key in RENAME", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.rename({} as any, "newkey"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'rename'."`); + }); + + test("should reject invalid new key in RENAME", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.rename("oldkey", null as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected newkey to be a string or buffer for 'rename'."`); + }); + + test("should reject invalid key in GETRANGE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.getrange({} as any, 0, 5); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'getrange'."`, + ); + }); + + test("should reject invalid key in SETRANGE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.setrange(undefined as any, 0, "value"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'setrange'."`, + ); + }); + + test("should reject invalid key in INCRBY", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.incrby([] as any, 10); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'incrby'."`); + }); + + test("should reject invalid value in MSET", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.mset("key", {} as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'mset'."`); + }); + + test("should reject invalid value in MSETNX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.msetnx("key1", "value1", "key2", [] as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'msetnx'."`); + }); + + test("should reject invalid key in SETBIT", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.setbit({} as any, 0, 1); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'setbit'."`); + }); + + test("should reject invalid key in SETEX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.setex(null as any, 10, "value"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'setex'."`); + }); + + test("should reject invalid key in PSETEX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.psetex([] as any, 1000, "value"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'psetex'."`); + }); + + test("should reject invalid key in UNLINK", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.unlink({} as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'unlink'."`); + }); + + test("should reject invalid additional key in UNLINK", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.unlink("valid-key", [] as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'unlink'."`); + }); + + test("should reject invalid key in TOUCH", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.touch(null as any); + }).toThrowErrorMatchingInlineSnapshot(`"The "key" argument must be specified"`); + }); + + test("should reject invalid additional key in TOUCH", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.touch("valid-key", {} as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'touch'."`); + }); }); describe("Connection State", () => { @@ -810,7 +1236,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { let currentMessage: any = {}; const subscriberProc = spawn({ - cmd: [self.process.execPath, "run", `${__dirname}/valkey.failing-subscriber.ts`], + cmd: [bunExe(), `${__dirname}/valkey.failing-subscriber.ts`], stdout: "inherit", stderr: "inherit", ipc: msg => { From e9731b2b1289a237159b74fdad2dc65efe34201c Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 20:33:33 -0700 Subject: [PATCH 03/45] feat: Implement expire commands --- packages/bun-types/redis.d.ts | 24 ++++++++ src/bun.js/api/valkey.classes.ts | 12 ++++ src/valkey/js_valkey.zig | 3 + src/valkey/js_valkey_functions.zig | 3 + test/js/valkey/valkey.test.ts | 88 ++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 2592908c0b77..823a84da758f 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -285,6 +285,22 @@ declare module "bun" { */ expire(key: RedisClient.KeyLike, seconds: number): Promise; + /** + * Set the expiration for a key as a Unix timestamp (in seconds) + * @param key The key to set expiration on + * @param timestamp Unix timestamp in seconds when the key should expire + * @returns Promise that resolves with 1 if timeout was set, 0 if key does not exist + */ + expireat(key: RedisClient.KeyLike, timestamp: number): Promise; + + /** + * Set a key's time to live in milliseconds + * @param key The key to set the expiration for + * @param milliseconds The number of milliseconds until expiration + * @returns Promise that resolves with 1 if the timeout was set, 0 if the key does not exist + */ + pexpire(key: RedisClient.KeyLike, milliseconds: number): Promise; + /** * Get the time to live for a key in seconds * @param key The key to get the TTL for @@ -443,6 +459,14 @@ declare module "bun" { */ persist(key: RedisClient.KeyLike): Promise; + /** + * Set the expiration for a key as a Unix timestamp in milliseconds + * @param key The key to set expiration on + * @param millisecondsTimestamp Unix timestamp in milliseconds when the key should expire + * @returns Promise that resolves with 1 if timeout was set, 0 if key does not exist + */ + pexpireat(key: RedisClient.KeyLike, millisecondsTimestamp: number): Promise; + /** * Get the expiration time of a key as a UNIX timestamp in milliseconds * @param key The key to check diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 7b4c6682f0c0..23613e641549 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -72,6 +72,14 @@ export default [ fn: "expire", length: 2, }, + expireat: { + fn: "expireat", + length: 2, + }, + pexpire: { + fn: "pexpire", + length: 2, + }, connect: { fn: "jsConnect", length: 0, @@ -183,6 +191,10 @@ export default [ persist: { fn: "persist", }, + pexpireat: { + fn: "pexpireat", + length: 2, + }, pexpiretime: { fn: "pexpiretime", }, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 3fbd28e44cb3..f5a5de1001c8 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1251,6 +1251,7 @@ pub const JSValkeyClient = struct { pub const duplicate = fns.duplicate; pub const exists = fns.exists; pub const expire = fns.expire; + pub const expireat = fns.expireat; pub const expiretime = fns.expiretime; pub const get = fns.get; pub const getBuffer = fns.getBuffer; @@ -1281,6 +1282,8 @@ pub const JSValkeyClient = struct { pub const mset = fns.mset; pub const msetnx = fns.msetnx; pub const persist = fns.persist; + pub const pexpire = fns.pexpire; + pub const pexpireat = fns.pexpireat; pub const pexpiretime = fns.pexpiretime; pub const pfadd = fns.pfadd; pub const ping = fns.ping; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 0c5241547420..db5bde9ec6e9 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -636,6 +636,7 @@ pub const setbit = compile.@"(...strings: string[])"("setbit", "SETBIT", .not_su pub const getrange = compile.@"(...strings: string[])"("getrange", "GETRANGE", .not_subscriber).call; pub const setrange = compile.@"(...strings: string[])"("setrange", "SETRANGE", .not_subscriber).call; pub const dump = compile.@"(key: RedisKey)"("dump", "DUMP", "key", .not_subscriber).call; +pub const expireat = compile.@"(key: RedisKey, value: RedisValue)"("expireat", "EXPIREAT", "key", "timestamp", .not_subscriber).call; pub const expiretime = compile.@"(key: RedisKey)"("expiretime", "EXPIRETIME", "key", .not_subscriber).call; pub const getdel = compile.@"(key: RedisKey)"("getdel", "GETDEL", "key", .not_subscriber).call; pub const getex = compile.@"(...strings: string[])"("getex", "GETEX", .not_subscriber).call; @@ -647,6 +648,8 @@ pub const keys = compile.@"(key: RedisKey)"("keys", "KEYS", "key", .not_subscrib pub const llen = compile.@"(key: RedisKey)"("llen", "LLEN", "key", .not_subscriber).call; pub const lpop = compile.@"(key: RedisKey)"("lpop", "LPOP", "key", .not_subscriber).call; pub const persist = compile.@"(key: RedisKey)"("persist", "PERSIST", "key", .not_subscriber).call; +pub const pexpire = compile.@"(key: RedisKey, value: RedisValue)"("pexpire", "PEXPIRE", "key", "milliseconds", .not_subscriber).call; +pub const pexpireat = compile.@"(key: RedisKey, value: RedisValue)"("pexpireat", "PEXPIREAT", "key", "milliseconds-timestamp", .not_subscriber).call; pub const pexpiretime = compile.@"(key: RedisKey)"("pexpiretime", "PEXPIRETIME", "key", .not_subscriber).call; pub const pttl = compile.@"(key: RedisKey)"("pttl", "PTTL", "key", .not_subscriber).call; pub const randomkey = compile.@"()"("randomkey", "RANDOMKEY", .not_subscriber).call; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index f6eb866d98b6..2a411e25fa62 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -334,6 +334,73 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(pttl).toBeLessThanOrEqual(5000); }); + test("should set expiration with EXPIREAT using Unix timestamp", async () => { + const redis = ctx.redis; + const key = "expireat-test-key"; + await redis.set(key, "test-value"); + + // Set expiration to 60 seconds from now using Unix timestamp + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; + const result = await redis.expireat(key, futureTimestamp); + expect(result).toBe(1); // 1 indicates success + + // Verify TTL is set (should be around 60 seconds) + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(60); + }); + + test("should return 0 for EXPIREAT on non-existent key", async () => { + const redis = ctx.redis; + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; + const result = await redis.expireat("nonexistent-expireat-key", futureTimestamp); + expect(result).toBe(0); // 0 indicates key does not exist + }); + + test("should set expiration with PEXPIRE in milliseconds", async () => { + const redis = ctx.redis; + const key = "pexpire-test-key"; + await redis.set(key, "test-value"); + + // Set expiration to 5000 milliseconds (5 seconds) + const result = await redis.pexpire(key, 5000); + expect(result).toBe(1); // 1 indicates success + + // Verify TTL is set using PTTL (should be around 5000 ms) + const pttl = await redis.pttl(key); + expect(pttl).toBeGreaterThan(0); + expect(pttl).toBeLessThanOrEqual(5000); + }); + + test("should return 0 for PEXPIRE on non-existent key", async () => { + const redis = ctx.redis; + const result = await redis.pexpire("nonexistent-pexpire-key", 5000); + expect(result).toBe(0); // 0 indicates key does not exist + }); + + test("should set expiration with PEXPIREAT using Unix timestamp in milliseconds", async () => { + const redis = ctx.redis; + const key = "pexpireat-test-key"; + await redis.set(key, "test-value"); + + // Set expiration to 5000 ms from now using Unix timestamp in milliseconds + const futureTimestampMs = Date.now() + 5000; + const result = await redis.pexpireat(key, futureTimestampMs); + expect(result).toBe(1); // 1 indicates success + + // Verify TTL is set using PTTL (should be around 5000 ms) + const pttl = await redis.pttl(key); + expect(pttl).toBeGreaterThan(0); + expect(pttl).toBeLessThanOrEqual(5000); + }); + + test("should return 0 for PEXPIREAT on non-existent key", async () => { + const redis = ctx.redis; + const futureTimestampMs = Date.now() + 5000; + const result = await redis.pexpireat("nonexistent-pexpireat-key", futureTimestampMs); + expect(result).toBe(0); // 0 indicates key does not exist + }); + test("should determine the type of a key with TYPE", async () => { const redis = ctx.redis; @@ -786,6 +853,27 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.touch("valid-key", {} as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'touch'."`); }); + + test("should reject invalid key in EXPIREAT", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.expireat({} as any, 1234567890); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'expireat'."`); + }); + + test("should reject invalid key in PEXPIRE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.pexpire([] as any, 5000); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'pexpire'."`); + }); + + test("should reject invalid key in PEXPIREAT", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.pexpireat(null as any, 1234567890000); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'pexpireat'."`); + }); }); describe("Connection State", () => { From 6ce6a1aafcd517d9d455418e47e9930f25ead6dd Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 20:50:59 -0700 Subject: [PATCH 04/45] feat: Implement the most simple sorted set commands --- packages/bun-types/redis.d.ts | 54 +++++++++ src/bun.js/api/valkey.classes.ts | 12 ++ src/valkey/js_valkey.zig | 6 + src/valkey/js_valkey_functions.zig | 6 + test/js/valkey/valkey.test.ts | 186 +++++++++++++++++++++++++++++ 5 files changed, 264 insertions(+) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 823a84da758f..a5fa408db26c 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -655,6 +655,24 @@ declare module "bun" { */ zcard(key: RedisClient.KeyLike): Promise; + /** + * Count the members in a sorted set with scores within the given range + * @param key The sorted set key + * @param min Minimum score (inclusive, use "-inf" for negative infinity) + * @param max Maximum score (inclusive, use "+inf" for positive infinity) + * @returns Promise that resolves with the count of elements in the specified score range + */ + zcount(key: RedisClient.KeyLike, min: string | number, max: string | number): Promise; + + /** + * Count the members in a sorted set within a lexicographical range + * @param key The sorted set key + * @param min Minimum value (use "[" for inclusive, "(" for exclusive, e.g., "[aaa") + * @param max Maximum value (use "[" for inclusive, "(" for exclusive, e.g., "[zzz") + * @returns Promise that resolves with the count of elements in the specified range + */ + zlexcount(key: RedisClient.KeyLike, min: string, max: string): Promise; + /** * Remove and return members with the highest scores in a sorted set * @param key The sorted set key @@ -790,6 +808,42 @@ declare module "bun" { */ zscore(key: RedisClient.KeyLike, member: string): Promise; + /** + * Increment the score of a member in a sorted set + * @param key The sorted set key + * @param increment The increment value + * @param member The member to increment + * @returns Promise that resolves with the new score + */ + zincrby(key: RedisClient.KeyLike, increment: number, member: RedisClient.KeyLike): Promise; + + /** + * Remove all members in a sorted set within the given lexicographical range + * @param key The sorted set key + * @param min Minimum value (use "[" for inclusive, "(" for exclusive, e.g., "[aaa") + * @param max Maximum value (use "[" for inclusive, "(" for exclusive, e.g., "[zzz") + * @returns Promise that resolves with the number of elements removed + */ + zremrangebylex(key: RedisClient.KeyLike, min: string, max: string): Promise; + + /** + * Remove all members in a sorted set within the given rank range + * @param key The sorted set key + * @param start Start rank (0-based, can be negative to indicate offset from end) + * @param stop Stop rank (0-based, can be negative to indicate offset from end) + * @returns Promise that resolves with the number of elements removed + */ + zremrangebyrank(key: RedisClient.KeyLike, start: number, stop: number): Promise; + + /** + * Remove all members in a sorted set within the given score range + * @param key The sorted set key + * @param min Minimum score (inclusive, use "-inf" for negative infinity, "(" prefix for exclusive) + * @param max Maximum score (inclusive, use "+inf" for positive infinity, "(" prefix for exclusive) + * @returns Promise that resolves with the number of elements removed + */ + zremrangebyscore(key: RedisClient.KeyLike, min: string | number, max: string | number): Promise; + /** * Get the values of all specified keys * @param keys The keys to get diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 23613e641549..a7e73bc2528f 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -222,6 +222,12 @@ export default [ zcard: { fn: "zcard", }, + zcount: { + fn: "zcount", + }, + zlexcount: { + fn: "zlexcount", + }, zpopmax: { fn: "zpopmax", }, @@ -264,6 +270,9 @@ export default [ zscore: { fn: "zscore", }, + zincrby: { + fn: "zincrby", + }, mget: { fn: "mget", }, @@ -282,6 +291,9 @@ export default [ substr: { fn: "substr" }, hstrlen: { fn: "hstrlen" }, zrank: { fn: "zrank" }, + zremrangebylex: { fn: "zremrangebylex" }, + zremrangebyrank: { fn: "zremrangebyrank" }, + zremrangebyscore: { fn: "zremrangebyscore" }, zrevrank: { fn: "zrevrank" }, subscribe: { fn: "subscribe" }, duplicate: { fn: "duplicate" }, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index f5a5de1001c8..9026b4e2b69c 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1324,12 +1324,18 @@ pub const JSValkeyClient = struct { pub const unlink = fns.unlink; pub const unsubscribe = fns.unsubscribe; pub const zcard = fns.zcard; + pub const zcount = fns.zcount; + pub const zlexcount = fns.zlexcount; pub const zpopmax = fns.zpopmax; pub const zpopmin = fns.zpopmin; pub const zrandmember = fns.zrandmember; pub const zrank = fns.zrank; + pub const zremrangebylex = fns.zremrangebylex; + pub const zremrangebyrank = fns.zremrangebyrank; + pub const zremrangebyscore = fns.zremrangebyscore; pub const zrevrank = fns.zrevrank; pub const zscore = fns.zscore; + pub const zincrby = fns.zincrby; const fns = @import("./js_valkey_functions.zig"); }; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index db5bde9ec6e9..895e98dc6b53 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -659,6 +659,8 @@ pub const scard = compile.@"(key: RedisKey)"("scard", "SCARD", "key", .not_subsc pub const strlen = compile.@"(key: RedisKey)"("strlen", "STRLEN", "key", .not_subscriber).call; pub const @"type" = compile.@"(key: RedisKey)"("type", "TYPE", "key", .not_subscriber).call; pub const zcard = compile.@"(key: RedisKey)"("zcard", "ZCARD", "key", .not_subscriber).call; +pub const zcount = compile.@"(...strings: string[])"("zcount", "ZCOUNT", .not_subscriber).call; +pub const zlexcount = compile.@"(...strings: string[])"("zlexcount", "ZLEXCOUNT", .not_subscriber).call; pub const zpopmax = compile.@"(key: RedisKey)"("zpopmax", "ZPOPMAX", "key", .not_subscriber).call; pub const zpopmin = compile.@"(key: RedisKey)"("zpopmin", "ZPOPMIN", "key", .not_subscriber).call; pub const zrandmember = compile.@"(key: RedisKey)"("zrandmember", "ZRANDMEMBER", "key", .not_subscriber).call; @@ -678,6 +680,7 @@ pub const setnx = compile.@"(key: RedisKey, value: RedisValue)"("setnx", "SETNX" pub const setex = compile.@"(...strings: string[])"("setex", "SETEX", .not_subscriber).call; pub const psetex = compile.@"(...strings: string[])"("psetex", "PSETEX", .not_subscriber).call; pub const zscore = compile.@"(key: RedisKey, value: RedisValue)"("zscore", "ZSCORE", "key", "value", .not_subscriber).call; +pub const zincrby = compile.@"(...strings: string[])"("zincrby", "ZINCRBY", .not_subscriber).call; pub const del = compile.@"(key: RedisKey, ...args: RedisKey[])"("del", "DEL", "key", .not_subscriber).call; pub const mget = compile.@"(key: RedisKey, ...args: RedisKey[])"("mget", "MGET", "key", .not_subscriber).call; @@ -691,6 +694,9 @@ pub const smove = compile.@"(...strings: string[])"("smove", "SMOVE", .not_subsc pub const substr = compile.@"(...strings: string[])"("substr", "SUBSTR", .not_subscriber).call; pub const hstrlen = compile.@"(...strings: string[])"("hstrlen", "HSTRLEN", .not_subscriber).call; pub const zrank = compile.@"(...strings: string[])"("zrank", "ZRANK", .not_subscriber).call; +pub const zremrangebylex = compile.@"(...strings: string[])"("zremrangebylex", "ZREMRANGEBYLEX", .not_subscriber).call; +pub const zremrangebyrank = compile.@"(...strings: string[])"("zremrangebyrank", "ZREMRANGEBYRANK", .not_subscriber).call; +pub const zremrangebyscore = compile.@"(...strings: string[])"("zremrangebyscore", "ZREMRANGEBYSCORE", .not_subscriber).call; pub const zrevrank = compile.@"(...strings: string[])"("zrevrank", "ZREVRANK", .not_subscriber).call; pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE", .dont_care).call; pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE", .dont_care).call; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 2a411e25fa62..14c080e56975 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -876,6 +876,192 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); }); + describe("Sorted Set Operations", () => { + test("should increment score with ZINCRBY", async () => { + const redis = ctx.redis; + const key = "zincrby-test"; + + // Add initial members + await redis.send("ZADD", [key, "1.0", "member1", "2.0", "member2"]); + + // Increment member1's score by 2.5 + const newScore1 = await redis.zincrby(key, 2.5, "member1"); + expect(newScore1).toBe(3.5); + + // Increment member2's score by -1.5 + const newScore2 = await redis.zincrby(key, -1.5, "member2"); + expect(newScore2).toBe(0.5); + + // Increment non-existent member (should create it with the increment as score) + const newScore3 = await redis.zincrby(key, 5, "member3"); + expect(newScore3).toBe(5); + }); + + test("should count members in score range with ZCOUNT", async () => { + const redis = ctx.redis; + const key = "zcount-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Count all members + const count1 = await redis.zcount(key, "-inf", "+inf"); + expect(count1).toBe(5); + + // Count members with score between 2 and 4 (inclusive) + const count2 = await redis.zcount(key, 2, 4); + expect(count2).toBe(3); // two, three, four + + // Count with specific range + const count3 = await redis.zcount(key, 1, 3); + expect(count3).toBe(3); // one, two, three + + // Count with no matches + const count4 = await redis.zcount(key, 10, 20); + expect(count4).toBe(0); + }); + + test("should count members in lexicographical range with ZLEXCOUNT", async () => { + const redis = ctx.redis; + const key = "zlexcount-test"; + + // Add members with same score (required for lex operations) + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + + // Count all members + const count1 = await redis.zlexcount(key, "-", "+"); + expect(count1).toBe(5); + + // Count members from "banana" to "date" (inclusive) + const count2 = await redis.zlexcount(key, "[banana", "[date"); + expect(count2).toBe(3); // banana, cherry, date + + // Count with exclusive range + const count3 = await redis.zlexcount(key, "(banana", "(date"); + expect(count3).toBe(1); // only cherry + + // Count with no matches + const count4 = await redis.zlexcount(key, "[zebra", "[zoo"); + expect(count4).toBe(0); + }); + + test("should remove members by rank with ZREMRANGEBYRANK", async () => { + const redis = ctx.redis; + const key = "zremrangebyrank-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Remove first 2 members (rank 0 and 1) + const removed1 = await redis.zremrangebyrank(key, 0, 1); + expect(removed1).toBe(2); + + // Verify remaining members + const remaining = await redis.send("ZCARD", [key]); + expect(remaining).toBe(3); + + // Remove last member (negative index) + const removed2 = await redis.zremrangebyrank(key, -1, -1); + expect(removed2).toBe(1); + + // Verify 2 members remain + const final = await redis.send("ZCARD", [key]); + expect(final).toBe(2); + }); + + test("should remove members by score range with ZREMRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zremrangebyscore-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Remove members with score 2-4 (inclusive) + const removed1 = await redis.zremrangebyscore(key, 2, 4); + expect(removed1).toBe(3); // two, three, four + + // Verify remaining members + const remaining = await redis.send("ZCARD", [key]); + expect(remaining).toBe(2); // one and five + + // Remove with infinity + const removed2 = await redis.zremrangebyscore(key, "-inf", "+inf"); + expect(removed2).toBe(2); + }); + + test("should remove members by lexicographical range with ZREMRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zremrangebylex-test"; + + // Add members with same score + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + + // Remove from "banana" to "date" (inclusive) + const removed1 = await redis.zremrangebylex(key, "[banana", "[date"); + expect(removed1).toBe(3); // banana, cherry, date + + // Verify remaining members + const remaining = await redis.send("ZCARD", [key]); + expect(remaining).toBe(2); // apple and elderberry + + // Remove remaining with open range + const removed2 = await redis.zremrangebylex(key, "-", "+"); + expect(removed2).toBe(2); + }); + + test("should reject invalid key in ZINCRBY", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zincrby({} as any, 1, "member"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zincrby'."`, + ); + }); + + test("should reject invalid key in ZCOUNT", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zcount([] as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zcount'."`); + }); + + test("should reject invalid key in ZLEXCOUNT", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zlexcount(null as any, "[a", "[z"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zlexcount'."`, + ); + }); + + test("should reject invalid key in ZREMRANGEBYRANK", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zremrangebyrank({} as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zremrangebyrank'."`, + ); + }); + + test("should reject invalid key in ZREMRANGEBYSCORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zremrangebyscore([] as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zremrangebyscore'."`, + ); + }); + + test("should reject invalid key in ZREMRANGEBYLEX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zremrangebylex(null as any, "[a", "[z"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zremrangebylex'."`, + ); + }); + }); + describe("Connection State", () => { test("should have a connected property", () => { const redis = ctx.redis; From bae58b9387044ca4e3f531d5ec09702ccf2be9d8 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 20:51:58 -0700 Subject: [PATCH 05/45] feat: Implement zmscore/zrem --- packages/bun-types/redis.d.ts | 18 ++++++++++++++++++ src/bun.js/api/valkey.classes.ts | 4 ++++ src/valkey/js_valkey.zig | 2 ++ src/valkey/js_valkey_functions.zig | 2 ++ 4 files changed, 26 insertions(+) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index a5fa408db26c..5ddb902bea14 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -817,6 +817,24 @@ declare module "bun" { */ zincrby(key: RedisClient.KeyLike, increment: number, member: RedisClient.KeyLike): Promise; + /** + * Returns the scores associated with the specified members in the sorted set + * @param key The sorted set key + * @param member The first member to get the score for + * @param members Additional members to get scores for + * @returns Promise that resolves with an array of scores (string for each score, or null if member doesn't exist) + */ + zmscore(key: RedisClient.KeyLike, member: RedisClient.KeyLike, ...members: RedisClient.KeyLike[]): Promise<(string | null)[]>; + + /** + * Remove one or more members from a sorted set + * @param key The sorted set key + * @param member The first member to remove + * @param members Additional members to remove + * @returns Promise that resolves with the number of members removed (not including non-existing members) + */ + zrem(key: RedisClient.KeyLike, member: RedisClient.KeyLike, ...members: RedisClient.KeyLike[]): Promise; + /** * Remove all members in a sorted set within the given lexicographical range * @param key The sorted set key diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index a7e73bc2528f..1fb974f05a76 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -273,6 +273,9 @@ export default [ zincrby: { fn: "zincrby", }, + zmscore: { + fn: "zmscore", + }, mget: { fn: "mget", }, @@ -291,6 +294,7 @@ export default [ substr: { fn: "substr" }, hstrlen: { fn: "hstrlen" }, zrank: { fn: "zrank" }, + zrem: { fn: "zrem" }, zremrangebylex: { fn: "zremrangebylex" }, zremrangebyrank: { fn: "zremrangebyrank" }, zremrangebyscore: { fn: "zremrangebyscore" }, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 9026b4e2b69c..8b36d9eb1991 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1330,12 +1330,14 @@ pub const JSValkeyClient = struct { pub const zpopmin = fns.zpopmin; pub const zrandmember = fns.zrandmember; pub const zrank = fns.zrank; + pub const zrem = fns.zrem; pub const zremrangebylex = fns.zremrangebylex; pub const zremrangebyrank = fns.zremrangebyrank; pub const zremrangebyscore = fns.zremrangebyscore; pub const zrevrank = fns.zrevrank; pub const zscore = fns.zscore; pub const zincrby = fns.zincrby; + pub const zmscore = fns.zmscore; const fns = @import("./js_valkey_functions.zig"); }; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 895e98dc6b53..71a0b4cf3141 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -681,6 +681,7 @@ pub const setex = compile.@"(...strings: string[])"("setex", "SETEX", .not_subsc pub const psetex = compile.@"(...strings: string[])"("psetex", "PSETEX", .not_subscriber).call; pub const zscore = compile.@"(key: RedisKey, value: RedisValue)"("zscore", "ZSCORE", "key", "value", .not_subscriber).call; pub const zincrby = compile.@"(...strings: string[])"("zincrby", "ZINCRBY", .not_subscriber).call; +pub const zmscore = compile.@"(...strings: string[])"("zmscore", "ZMSCORE", .not_subscriber).call; pub const del = compile.@"(key: RedisKey, ...args: RedisKey[])"("del", "DEL", "key", .not_subscriber).call; pub const mget = compile.@"(key: RedisKey, ...args: RedisKey[])"("mget", "MGET", "key", .not_subscriber).call; @@ -694,6 +695,7 @@ pub const smove = compile.@"(...strings: string[])"("smove", "SMOVE", .not_subsc pub const substr = compile.@"(...strings: string[])"("substr", "SUBSTR", .not_subscriber).call; pub const hstrlen = compile.@"(...strings: string[])"("hstrlen", "HSTRLEN", .not_subscriber).call; pub const zrank = compile.@"(...strings: string[])"("zrank", "ZRANK", .not_subscriber).call; +pub const zrem = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrem", "ZREM", "key", .not_subscriber).call; pub const zremrangebylex = compile.@"(...strings: string[])"("zremrangebylex", "ZREMRANGEBYLEX", .not_subscriber).call; pub const zremrangebyrank = compile.@"(...strings: string[])"("zremrangebyrank", "ZREMRANGEBYRANK", .not_subscriber).call; pub const zremrangebyscore = compile.@"(...strings: string[])"("zremrangebyscore", "ZREMRANGEBYSCORE", .not_subscriber).call; From 781e57a0394f8e625ef6f74d704b8af08afdf224 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 03:55:05 +0000 Subject: [PATCH 06/45] [autofix.ci] apply automated fixes --- packages/bun-types/redis.d.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 5ddb902bea14..6b8cf919f99a 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -824,7 +824,11 @@ declare module "bun" { * @param members Additional members to get scores for * @returns Promise that resolves with an array of scores (string for each score, or null if member doesn't exist) */ - zmscore(key: RedisClient.KeyLike, member: RedisClient.KeyLike, ...members: RedisClient.KeyLike[]): Promise<(string | null)[]>; + zmscore( + key: RedisClient.KeyLike, + member: RedisClient.KeyLike, + ...members: RedisClient.KeyLike[] + ): Promise<(string | null)[]>; /** * Remove one or more members from a sorted set From 1d46d3fe63398e3d97b3c65ed4420afbc1b7aa76 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 20:57:44 -0700 Subject: [PATCH 07/45] update zrem & zmscore --- packages/bun-types/redis.d.ts | 4 +-- test/js/valkey/valkey.test.ts | 62 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 5ddb902bea14..d0cd1cf52b7b 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -822,9 +822,9 @@ declare module "bun" { * @param key The sorted set key * @param member The first member to get the score for * @param members Additional members to get scores for - * @returns Promise that resolves with an array of scores (string for each score, or null if member doesn't exist) + * @returns Promise that resolves with an array of scores (number for each score, or null if member doesn't exist) */ - zmscore(key: RedisClient.KeyLike, member: RedisClient.KeyLike, ...members: RedisClient.KeyLike[]): Promise<(string | null)[]>; + zmscore(key: RedisClient.KeyLike, member: RedisClient.KeyLike, ...members: RedisClient.KeyLike[]): Promise<(number | null)[]>; /** * Remove one or more members from a sorted set diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 14c080e56975..fdd2a4da97db 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -1060,6 +1060,68 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { `"Expected additional arguments to be a string or buffer for 'zremrangebylex'."`, ); }); + + test("should remove one or more members with ZREM", async () => { + const redis = ctx.redis; + const key = "zrem-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); + + // Remove single member + const removed1 = await redis.zrem(key, "two"); + expect(removed1).toBe(1); + + // Remove multiple members + const removed2 = await redis.zrem(key, "one", "three"); + expect(removed2).toBe(2); + + // Remove non-existent member + const removed3 = await redis.zrem(key, "nonexistent"); + expect(removed3).toBe(0); + + // Remove mix of existing and non-existing + const removed4 = await redis.zrem(key, "four", "nothere"); + expect(removed4).toBe(1); // Only "four" was removed + }); + + test("should get scores with ZMSCORE", async () => { + const redis = ctx.redis; + const key = "zmscore-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + // Get single score + const scores1 = await redis.zmscore(key, "two"); + expect(scores1).toEqual([2.7]); + + // Get multiple scores + const scores2 = await redis.zmscore(key, "one", "three"); + expect(scores2).toEqual([1.5, 3.9]); + + // Get mix of existing and non-existing members + const scores3 = await redis.zmscore(key, "one", "nonexistent", "three"); + expect(scores3).toEqual([1.5, null, 3.9]); + + // Get all non-existent members + const scores4 = await redis.zmscore(key, "nothere", "alsonothere"); + expect(scores4).toEqual([null, null]); + }); + + test("should reject invalid key in ZREM", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrem({} as any, "member"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zrem'."`); + }); + + test("should reject invalid key in ZMSCORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zmscore([] as any, "member"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zmscore'."`); + }); }); describe("Connection State", () => { From 02b051f4828f377f0ff3b01fe56d8af28c27abd4 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 21:22:57 -0700 Subject: [PATCH 08/45] zrange/zrevrange/zrangebyscore/zrevrangebyscore/zrangebylex/zrevrangebylex/zadd/zscan/zrangestore --- packages/bun-types/redis.d.ts | 620 ++++++++++++++++++++++- src/bun.js/api/valkey.classes.ts | 36 ++ src/valkey/js_valkey.zig | 9 + src/valkey/js_valkey_functions.zig | 9 + test/js/valkey/valkey.test.ts | 781 ++++++++++++++++++++++++++++- 5 files changed, 1452 insertions(+), 3 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index d0cd1cf52b7b..a271c847429c 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -697,6 +697,171 @@ declare module "bun" { */ zrandmember(key: RedisClient.KeyLike): Promise; + /** + * Return a range of members in a sorted set with their scores + * + * @param key The sorted set key + * @param start The starting index + * @param stop The stopping index + * @param withscores Return members with their scores + * @returns Promise that resolves with an array of [member, score, member, score, ...] + * + * @example + * ```ts + * const results = await redis.zrange("myzset", 0, -1, "WITHSCORES"); + * // Returns ["member1", "1.5", "member2", "2.5", ...] + * ``` + */ + zrange( + key: RedisClient.KeyLike, + start: string | number, + stop: string | number, + withscores: "WITHSCORES", + ): Promise<[string, number][]>; + + /** + * Return a range of members in a sorted set by score + * + * @param key The sorted set key + * @param start The minimum score (use "-inf" for negative infinity, "(" prefix for exclusive) + * @param stop The maximum score (use "+inf" for positive infinity, "(" prefix for exclusive) + * @param byscore Indicates score-based range + * @returns Promise that resolves with an array of members with scores in the range + * + * @example + * ```ts + * // Get members with score between 1 and 3 + * const members = await redis.zrange("myzset", "1", "3", "BYSCORE"); + * + * // Get members with score > 1 and <= 3 (exclusive start) + * const members2 = await redis.zrange("myzset", "(1", "3", "BYSCORE"); + * ``` + */ + zrange( + key: RedisClient.KeyLike, + start: string | number, + stop: string | number, + byscore: "BYSCORE", + ): Promise; + + /** + * Return a range of members in a sorted set lexicographically + * + * @param key The sorted set key + * @param start The minimum lexicographical value (use "-" for start, "[" for inclusive, "(" for exclusive) + * @param stop The maximum lexicographical value (use "+" for end, "[" for inclusive, "(" for exclusive) + * @param bylex Indicates lexicographical range + * @returns Promise that resolves with an array of members in the lexicographical range + * + * @example + * ```ts + * // Get members lexicographically from "a" to "c" (inclusive) + * const members = await redis.zrange("myzset", "[a", "[c", "BYLEX"); + * ``` + */ + zrange(key: RedisClient.KeyLike, start: string, stop: string, bylex: "BYLEX"): Promise; + + /** + * Return a range of members in a sorted set with various options + * + * @param key The sorted set key + * @param start The starting value (index, score, or lex depending on options) + * @param stop The stopping value + * @param options Additional options (BYSCORE, BYLEX, REV, LIMIT offset count, WITHSCORES) + * @returns Promise that resolves with an array of members (or with scores if WITHSCORES) + * + * @example + * ```ts + * // Get members by score with limit + * const members = await redis.zrange("myzset", "1", "10", "BYSCORE", "LIMIT", "0", "5"); + * + * // Get members in reverse order with scores + * const reversed = await redis.zrange("myzset", "0", "-1", "REV", "WITHSCORES"); + * ``` + */ + zrange( + key: RedisClient.KeyLike, + start: string | number, + stop: string | number, + ...options: string[] + ): Promise; + + /** + * Return a range of members in a sorted set + * + * Returns the specified range of elements in the sorted set stored at key. + * The elements are considered to be ordered from the lowest to the highest score by default. + * + * @param key The sorted set key + * @param start The starting index (0-based, can be negative to count from end) + * @param stop The stopping index (0-based, can be negative to count from end) + * @returns Promise that resolves with an array of members in the specified range + * + * @example + * ```ts + * // Get all members + * const members = await redis.zrange("myzset", 0, -1); + * + * // Get first 3 members + * const top3 = await redis.zrange("myzset", 0, 2); + * ``` + */ + zrange(key: RedisClient.KeyLike, start: string | number, stop: string | number): Promise; + + /** + * Return a range of members in a sorted set, by index, with scores ordered from high to low + * + * This is equivalent to ZRANGE with the REV option. Returns members in reverse order. + * + * @param key The sorted set key + * @param start The starting index (0-based, can be negative to count from end) + * @param stop The stopping index (0-based, can be negative to count from end) + * @returns Promise that resolves with an array of members in reverse order + * + * @example + * ```ts + * // Get all members in reverse order (highest to lowest score) + * const members = await redis.zrevrange("myzset", 0, -1); + * + * // Get top 3 members with highest scores + * const top3 = await redis.zrevrange("myzset", 0, 2); + * ``` + */ + zrevrange(key: RedisClient.KeyLike, start: number, stop: number): Promise; + + /** + * Return a range of members in a sorted set with their scores, ordered from high to low + * + * @param key The sorted set key + * @param start The starting index + * @param stop The stopping index + * @param withscores Return members with their scores + * @returns Promise that resolves with an array of [member, score, member, score, ...] in reverse order + * + * @example + * ```ts + * const results = await redis.zrevrange("myzset", 0, -1, "WITHSCORES"); + * // Returns ["member3", "3.5", "member2", "2.5", "member1", "1.5", ...] + * ``` + */ + zrevrange( + key: RedisClient.KeyLike, + start: number, + stop: number, + withscores: "WITHSCORES", + ): Promise<[string, number][]>; + + /** + * Return a range of members in a sorted set with options, ordered from high to low + * + * @param key The sorted set key + * @param start The starting index + * @param stop The stopping index + * @param options Additional options (WITHSCORES) + * @returns Promise that resolves with an array of members (or with scores if WITHSCORES) + */ + zrevrange(key: RedisClient.KeyLike, start: number, stop: number, ...options: string[]): Promise; + /** * Append a value to a key * @param key The key to append to @@ -806,7 +971,7 @@ declare module "bun" { * @returns Promise that resolves with the score of the member as a string, * or null if the member or key doesn't exist */ - zscore(key: RedisClient.KeyLike, member: string): Promise; + zscore(key: RedisClient.KeyLike, member: string): Promise; /** * Increment the score of a member in a sorted set @@ -824,7 +989,113 @@ declare module "bun" { * @param members Additional members to get scores for * @returns Promise that resolves with an array of scores (number for each score, or null if member doesn't exist) */ - zmscore(key: RedisClient.KeyLike, member: RedisClient.KeyLike, ...members: RedisClient.KeyLike[]): Promise<(number | null)[]>; + zmscore( + key: RedisClient.KeyLike, + member: RedisClient.KeyLike, + ...members: RedisClient.KeyLike[] + ): Promise<(number | null)[]>; + + /** + * Add one or more members to a sorted set, or update scores if they already exist + * + * ZADD adds all the specified members with the specified scores to the sorted set stored at key. + * It is possible to specify multiple score / member pairs. If a specified member is already a + * member of the sorted set, the score is updated and the element reinserted at the right position + * to ensure the correct ordering. + * + * If key does not exist, a new sorted set with the specified members as sole members is created. + * If the key exists but does not hold a sorted set, an error is returned. + * + * The score values should be the string representation of a double precision floating point number. + * +inf and -inf values are valid values as well. + * + * Options: + * - NX: Only add new elements. Don't update already existing elements. + * - XX: Only update elements that already exist. Never add elements. + * - GT: Only update existing elements if the new score is greater than the current score. This flag doesn't prevent adding new elements. + * - LT: Only update existing elements if the new score is less than the current score. This flag doesn't prevent adding new elements. + * - CH: Modify the return value from the number of new elements added, to the total number of elements changed (CH is an abbreviation of changed). + * - INCR: When this option is specified ZADD acts like ZINCRBY. Only one score-member pair can be specified in this mode. + * + * Note: The GT, LT and NX options are mutually exclusive. + * + * @param key The sorted set key + * @param args Score-member pairs and optional flags (NX, XX, GT, LT, CH, INCR) + * @returns Promise that resolves with the number of elements added (or changed if CH is used, or new score if INCR is used) + * + * @example + * ```ts + * // Add members with scores + * await redis.zadd("myzset", "1", "one", "2", "two", "3", "three"); + * + * // Add with NX option (only if member doesn't exist) + * await redis.zadd("myzset", "NX", "4", "four"); + * + * // Add with XX option (only if member exists) + * await redis.zadd("myzset", "XX", "2.5", "two"); + * + * // Add with CH option (return count of changed elements) + * await redis.zadd("myzset", "CH", "5", "five", "2.1", "two"); + * + * // Use INCR option (increment score) + * await redis.zadd("myzset", "INCR", "1.5", "one"); + * ``` + */ + zadd(key: RedisClient.KeyLike, ...args: (string | number)[]): Promise; + + /** + * Incrementally iterate sorted set elements and their scores + * + * The ZSCAN command is used in order to incrementally iterate over sorted set elements and their scores. + * ZSCAN is a cursor based iterator. This means that at every call of the command, the server returns an + * updated cursor that the user needs to use as the cursor argument in the next call. + * + * An iteration starts when the cursor is set to 0, and terminates when the cursor returned by the server is 0. + * + * ZSCAN and the other SCAN family commands are able to provide to the user a set of guarantees associated + * to full iterations: + * - A full iteration always retrieves all the elements that were present in the collection from the start + * to the end of a full iteration. This means that if a given element is inside the collection when an + * iteration is started, and is still there when an iteration terminates, then at some point ZSCAN returned it. + * - A full iteration never returns any element that was NOT present in the collection from the start to the + * end of a full iteration. So if an element was removed before the start of an iteration, and is never + * added back to the collection for all the time an iteration lasts, ZSCAN ensures that this element will + * never be returned. + * + * Options: + * - MATCH pattern: Only return elements matching the pattern (glob-style) + * - COUNT count: Amount of work done at every call (hint, not exact) + * + * @param key The sorted set key + * @param cursor The cursor value (use 0 to start a new iteration) + * @param options Additional ZSCAN options (MATCH pattern, COUNT hint, etc.) + * @returns Promise that resolves with a tuple [cursor, [member1, score1, member2, score2, ...]] + * + * @example + * ```ts + * // Basic scan - iterate all elements + * let cursor = "0"; + * const allElements: string[] = []; + * do { + * const [nextCursor, elements] = await redis.zscan("myzset", cursor); + * allElements.push(...elements); + * cursor = nextCursor; + * } while (cursor !== "0"); + * ``` + * + * @example + * ```ts + * // Scan with MATCH pattern + * const [cursor, elements] = await redis.zscan("myzset", "0", "MATCH", "user:*"); + * ``` + * + * @example + * ```ts + * // Scan with COUNT hint + * const [cursor, elements] = await redis.zscan("myzset", "0", "COUNT", "100"); + * ``` + */ + zscan(key: RedisClient.KeyLike, cursor: string | number, ...options: string[]): Promise<[string, string[]]>; /** * Remove one or more members from a sorted set @@ -862,6 +1133,351 @@ declare module "bun" { */ zremrangebyscore(key: RedisClient.KeyLike, min: string | number, max: string | number): Promise; + /** + * Return members in a sorted set within a lexicographical range + * + * When all the elements in a sorted set have the same score, this command + * returns the elements between min and max in lexicographical order. + * + * Lex ranges: + * - `[member` for inclusive lower bound + * - `(member` for exclusive lower bound + * - `-` for negative infinity + * - `+` for positive infinity + * + * @param key The sorted set key (all members must have the same score) + * @param min Minimum lexicographical value (use "-" for negative infinity, "[" or "(" for inclusive/exclusive) + * @param max Maximum lexicographical value (use "+" for positive infinity, "[" or "(" for inclusive/exclusive) + * @returns Promise that resolves with array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "0", "apple", "0", "banana", "0", "cherry"]); + * const members = await redis.zrangebylex("myzset", "[banana", "[cherry"); + * // Returns: ["banana", "cherry"] + * ``` + */ + zrangebylex(key: RedisClient.KeyLike, min: string, max: string): Promise; + + /** + * Return members in a sorted set within a lexicographical range, with pagination + * + * @param key The sorted set key + * @param min Minimum lexicographical value + * @param max Maximum lexicographical value + * @param limit The "LIMIT" keyword + * @param offset The number of elements to skip + * @param count The maximum number of elements to return + * @returns Promise that resolves with array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "0", "a", "0", "b", "0", "c", "0", "d"]); + * const result = await redis.zrangebylex("myzset", "-", "+", "LIMIT", 1, 2); + * // Returns: ["b", "c"] + * ``` + */ + zrangebylex( + key: RedisClient.KeyLike, + min: string, + max: string, + limit: "LIMIT", + offset: number, + count: number, + ): Promise; + + /** + * Return members in a sorted set within a lexicographical range, with options + * + * @param key The sorted set key + * @param min Minimum lexicographical value + * @param max Maximum lexicographical value + * @param options Additional options (LIMIT offset count) + * @returns Promise that resolves with array of members + */ + zrangebylex(key: RedisClient.KeyLike, min: string, max: string, ...options: (string | number)[]): Promise; + + /** + * Return members in a sorted set with scores within a given range + * + * Returns all the elements in the sorted set at key with a score between min + * and max (inclusive by default). The elements are considered to be ordered + * from low to high scores. + * + * Score ranges support: + * - `-inf` and `+inf` for negative and positive infinity + * - `(` prefix for exclusive bounds (e.g., `(5` means greater than 5, not including 5) + * + * @param key The sorted set key + * @param min Minimum score (can be "-inf", a number, or prefixed with "(" for exclusive) + * @param max Maximum score (can be "+inf", a number, or prefixed with "(" for exclusive) + * @returns Promise that resolves with array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "1", "one", "2", "two", "3", "three"]); + * const members = await redis.zrangebyscore("myzset", 1, 2); + * // Returns: ["one", "two"] + * ``` + */ + zrangebyscore(key: RedisClient.KeyLike, min: string | number, max: string | number): Promise; + + /** + * Return members in a sorted set with scores within a given range, with scores + * + * @param key The sorted set key + * @param min Minimum score + * @param max Maximum score + * @param withscores The "WITHSCORES" keyword to return scores along with members + * @returns Promise that resolves with array of [member, score, member, score, ...] + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "1", "one", "2", "two", "3", "three"]); + * const result = await redis.zrangebyscore("myzset", 1, 2, "WITHSCORES"); + * // Returns: ["one", "1", "two", "2"] + * ``` + */ + zrangebyscore( + key: RedisClient.KeyLike, + min: string | number, + max: string | number, + withscores: "WITHSCORES", + ): Promise<[string, number][]>; + + /** + * Return members in a sorted set with scores within a given range, with pagination + * + * @param key The sorted set key + * @param min Minimum score + * @param max Maximum score + * @param limit The "LIMIT" keyword + * @param offset The number of elements to skip + * @param count The maximum number of elements to return + * @returns Promise that resolves with array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "1", "one", "2", "two", "3", "three", "4", "four"]); + * const result = await redis.zrangebyscore("myzset", "-inf", "+inf", "LIMIT", 1, 2); + * // Returns: ["two", "three"] + * ``` + */ + zrangebyscore( + key: RedisClient.KeyLike, + min: string | number, + max: string | number, + limit: "LIMIT", + offset: number, + count: number, + ): Promise; + + /** + * Return members in a sorted set with scores within a given range, with the score values + * + * @param key The sorted set key + * @param min Minimum score + * @param max Maximum score + * @param options Additional options (WITHSCORES, LIMIT offset count) + * @returns Promise that resolves with array of members (and scores if WITHSCORES is used) + */ + zrangebyscore( + key: RedisClient.KeyLike, + min: string | number, + max: string | number, + withscores: "WITHSCORES", + ...options: (string | number)[] + ): Promise<[string, number][]>; + + /** + * Return members in a sorted set with scores within a given range, with the score values + * + * @param key The sorted set key + * @param min Minimum score + * @param max Maximum score + * @param options Additional options (WITHSCORES, LIMIT offset count) + * @returns Promise that resolves with array of members (and scores if WITHSCORES is used) + */ + zrangebyscore( + key: RedisClient.KeyLike, + min: string | number, + max: string | number, + withscores: "WITHSCORES", + limit: "LIMIT", + offset: number, + count: number, + ...options: (string | number)[] + ): Promise<[string, number][]>; + + /** + * Return members in a sorted set with scores within a given range, with various options + * + * @param key The sorted set key + * @param min Minimum score + * @param max Maximum score + * @param options Additional options (WITHSCORES, LIMIT offset count) + * @returns Promise that resolves with array of members (and scores if WITHSCORES is used) + */ + zrangebyscore( + key: RedisClient.KeyLike, + min: string | number, + max: string | number, + ...options: (string | number)[] + ): Promise; + + /** + * Return members in a sorted set with scores within a given range, ordered from high to low + * + * Returns all the elements in the sorted set at key with a score between max + * and min (note: max comes before min). The elements are considered to be + * ordered from high to low scores. + * + * Score ranges support: + * - `-inf` and `+inf` for negative and positive infinity + * - `(` prefix for exclusive bounds (e.g., `(5` means less than 5, not including 5) + * + * @param key The sorted set key + * @param max Maximum score (can be "+inf", a number, or prefixed with "(" for exclusive) + * @param min Minimum score (can be "-inf", a number, or prefixed with "(" for exclusive) + * @returns Promise that resolves with array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "1", "one", "2", "two", "3", "three"]); + * const members = await redis.zrevrangebyscore("myzset", 2, 1); + * // Returns: ["two", "one"] + * ``` + */ + zrevrangebyscore(key: RedisClient.KeyLike, max: string | number, min: string | number): Promise; + + /** + * Return members in a sorted set with scores within a given range, ordered from high to low, with scores + * + * @param key The sorted set key + * @param max Maximum score + * @param min Minimum score + * @param withscores The "WITHSCORES" keyword to return scores along with members + * @returns Promise that resolves with array of [member, score, member, score, ...] + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "1", "one", "2", "two", "3", "three"]); + * const result = await redis.zrevrangebyscore("myzset", 2, 1, "WITHSCORES"); + * // Returns: ["two", "2", "one", "1"] + * ``` + */ + zrevrangebyscore( + key: RedisClient.KeyLike, + max: string | number, + min: string | number, + withscores: "WITHSCORES", + ): Promise<[string, number][]>; + + /** + * Return members in a sorted set with scores within a given range, ordered from high to low, with pagination + * + * @param key The sorted set key + * @param max Maximum score + * @param min Minimum score + * @param limit The "LIMIT" keyword + * @param offset The number of elements to skip + * @param count The maximum number of elements to return + * @returns Promise that resolves with array of members + */ + zrevrangebyscore( + key: RedisClient.KeyLike, + max: string | number, + min: string | number, + limit: "LIMIT", + offset: number, + count: number, + ): Promise; + + /** + * Return members in a sorted set with scores within a given range, ordered from high to low, with options + * + * @param key The sorted set key + * @param max Maximum score + * @param min Minimum score + * @param options Additional options (WITHSCORES, LIMIT offset count) + * @returns Promise that resolves with array of members (and scores if WITHSCORES is used) + */ + zrevrangebyscore( + key: RedisClient.KeyLike, + max: string | number, + min: string | number, + ...options: (string | number)[] + ): Promise; + + /** + * Return members in a sorted set within a lexicographical range, ordered from high to low + * + * All members in a sorted set must have the same score for this command to work correctly. + * The max and min arguments have the same meaning as in ZRANGEBYLEX, but in reverse order. + * + * Use "[" for inclusive bounds and "(" for exclusive bounds. Use "-" for negative infinity and "+" for positive infinity. + * + * @param key The sorted set key + * @param max The maximum lexicographical value (inclusive with "[", exclusive with "(") + * @param min The minimum lexicographical value (inclusive with "[", exclusive with "(") + * @param options Optional LIMIT clause: ["LIMIT", offset, count] + * @returns Promise that resolves with an array of members in reverse lexicographical order + * + * @example + * ```ts + * // Add members with same score + * await redis.send("ZADD", ["myzset", "0", "a", "0", "b", "0", "c", "0", "d"]); + * + * // Get range from highest to lowest + * const members = await redis.zrevrangebylex("myzset", "[d", "[b"); + * console.log(members); // ["d", "c", "b"] + * + * // With LIMIT + * const limited = await redis.zrevrangebylex("myzset", "+", "-", "LIMIT", "0", "2"); + * console.log(limited); // ["d", "c"] (first 2 members) + * ``` + */ + zrevrangebylex(key: RedisClient.KeyLike, max: string, min: string, ...options: string[]): Promise; + + /** + * Store a range of members from a sorted set into a destination key + * + * This command is like ZRANGE but stores the result in a destination key instead of returning it. + * Supports all the same options as ZRANGE including BYSCORE, BYLEX, REV, and LIMIT. + * + * @param destination The destination key to store results + * @param source The source sorted set key + * @param start The starting index or score + * @param stop The ending index or score + * @param options Optional flags: ["BYSCORE"], ["BYLEX"], ["REV"], ["LIMIT", offset, count] + * @returns Promise that resolves with the number of elements in the resulting sorted set + * + * @example + * ```ts + * // Add members to source set + * await redis.send("ZADD", ["source", "1", "one", "2", "two", "3", "three"]); + * + * // Store range by rank + * const count1 = await redis.zrangestore("dest1", "source", 0, 1); + * console.log(count1); // 2 + * + * // Store range by score + * const count2 = await redis.zrangestore("dest2", "source", "1", "2", "BYSCORE"); + * console.log(count2); // 2 + * + * // Store in reverse order with limit + * const count3 = await redis.zrangestore("dest3", "source", "0", "-1", "REV", "LIMIT", "0", "2"); + * console.log(count3); // 2 + * ``` + */ + zrangestore( + destination: RedisClient.KeyLike, + source: RedisClient.KeyLike, + start: string | number, + stop: string | number, + ...options: string[] + ): Promise; + /** * Get the values of all specified keys * @param keys The keys to get diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 1fb974f05a76..7ae74834f27d 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -237,6 +237,34 @@ export default [ zrandmember: { fn: "zrandmember", }, + zrange: { + fn: "zrange", + length: 3, + }, + zrangebylex: { + fn: "zrangebylex", + length: 3, + }, + zrangebyscore: { + fn: "zrangebyscore", + length: 3, + }, + zrangestore: { + fn: "zrangestore", + length: 4, + }, + zrevrange: { + fn: "zrevrange", + length: 3, + }, + zrevrangebylex: { + fn: "zrevrangebylex", + length: 3, + }, + zrevrangebyscore: { + fn: "zrevrangebyscore", + length: 3, + }, append: { fn: "append", }, @@ -276,6 +304,14 @@ export default [ zmscore: { fn: "zmscore", }, + zadd: { + fn: "zadd", + length: 3, + }, + zscan: { + fn: "zscan", + length: 2, + }, mget: { fn: "mget", }, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 8b36d9eb1991..56e8b569888c 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1329,15 +1329,24 @@ pub const JSValkeyClient = struct { pub const zpopmax = fns.zpopmax; pub const zpopmin = fns.zpopmin; pub const zrandmember = fns.zrandmember; + pub const zrange = fns.zrange; + pub const zrangebylex = fns.zrangebylex; + pub const zrangebyscore = fns.zrangebyscore; + pub const zrangestore = fns.zrangestore; pub const zrank = fns.zrank; pub const zrem = fns.zrem; pub const zremrangebylex = fns.zremrangebylex; pub const zremrangebyrank = fns.zremrangebyrank; pub const zremrangebyscore = fns.zremrangebyscore; + pub const zrevrange = fns.zrevrange; + pub const zrevrangebylex = fns.zrevrangebylex; + pub const zrevrangebyscore = fns.zrevrangebyscore; pub const zrevrank = fns.zrevrank; pub const zscore = fns.zscore; pub const zincrby = fns.zincrby; pub const zmscore = fns.zmscore; + pub const zadd = fns.zadd; + pub const zscan = fns.zscan; const fns = @import("./js_valkey_functions.zig"); }; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 71a0b4cf3141..d07cd1ec432b 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -664,6 +664,12 @@ pub const zlexcount = compile.@"(...strings: string[])"("zlexcount", "ZLEXCOUNT" pub const zpopmax = compile.@"(key: RedisKey)"("zpopmax", "ZPOPMAX", "key", .not_subscriber).call; pub const zpopmin = compile.@"(key: RedisKey)"("zpopmin", "ZPOPMIN", "key", .not_subscriber).call; pub const zrandmember = compile.@"(key: RedisKey)"("zrandmember", "ZRANDMEMBER", "key", .not_subscriber).call; +pub const zrange = compile.@"(...strings: string[])"("zrange", "ZRANGE", .not_subscriber).call; +pub const zrevrange = compile.@"(...strings: string[])"("zrevrange", "ZREVRANGE", .not_subscriber).call; +pub const zrangebyscore = compile.@"(...strings: string[])"("zrangebyscore", "ZRANGEBYSCORE", .not_subscriber).call; +pub const zrevrangebyscore = compile.@"(...strings: string[])"("zrevrangebyscore", "ZREVRANGEBYSCORE", .not_subscriber).call; +pub const zrangebylex = compile.@"(...strings: string[])"("zrangebylex", "ZRANGEBYLEX", .not_subscriber).call; +pub const zrevrangebylex = compile.@"(...strings: string[])"("zrevrangebylex", "ZREVRANGEBYLEX", .not_subscriber).call; pub const append = compile.@"(key: RedisKey, value: RedisValue)"("append", "APPEND", "key", "value", .not_subscriber).call; pub const getset = compile.@"(key: RedisKey, value: RedisValue)"("getset", "GETSET", "key", "value", .not_subscriber).call; @@ -682,6 +688,8 @@ pub const psetex = compile.@"(...strings: string[])"("psetex", "PSETEX", .not_su pub const zscore = compile.@"(key: RedisKey, value: RedisValue)"("zscore", "ZSCORE", "key", "value", .not_subscriber).call; pub const zincrby = compile.@"(...strings: string[])"("zincrby", "ZINCRBY", .not_subscriber).call; pub const zmscore = compile.@"(...strings: string[])"("zmscore", "ZMSCORE", .not_subscriber).call; +pub const zadd = compile.@"(...strings: string[])"("zadd", "ZADD", .not_subscriber).call; +pub const zscan = compile.@"(...strings: string[])"("zscan", "ZSCAN", .not_subscriber).call; pub const del = compile.@"(key: RedisKey, ...args: RedisKey[])"("del", "DEL", "key", .not_subscriber).call; pub const mget = compile.@"(key: RedisKey, ...args: RedisKey[])"("mget", "MGET", "key", .not_subscriber).call; @@ -695,6 +703,7 @@ pub const smove = compile.@"(...strings: string[])"("smove", "SMOVE", .not_subsc pub const substr = compile.@"(...strings: string[])"("substr", "SUBSTR", .not_subscriber).call; pub const hstrlen = compile.@"(...strings: string[])"("hstrlen", "HSTRLEN", .not_subscriber).call; pub const zrank = compile.@"(...strings: string[])"("zrank", "ZRANK", .not_subscriber).call; +pub const zrangestore = compile.@"(...strings: string[])"("zrangestore", "ZRANGESTORE", .not_subscriber).call; pub const zrem = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrem", "ZREM", "key", .not_subscriber).call; pub const zremrangebylex = compile.@"(...strings: string[])"("zremrangebylex", "ZREMRANGEBYLEX", .not_subscriber).call; pub const zremrangebyrank = compile.@"(...strings: string[])"("zremrangebyrank", "ZREMRANGEBYRANK", .not_subscriber).call; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index fdd2a4da97db..6a1c44a64b02 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -1120,7 +1120,786 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { await redis.zmscore([] as any, "member"); - }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zmscore'."`); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zmscore'."`, + ); + }); + + test("should add members to sorted set with ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-basic-test"; + + // Add single member + const added1 = await redis.zadd(key, "1", "one"); + expect(added1).toBe(1); + + // Add multiple members + const added2 = await redis.zadd(key, "2", "two", "3", "three"); + expect(added2).toBe(2); + + // Update existing member (should return 0 since no new members added) + const added3 = await redis.zadd(key, "1.5", "one"); + expect(added3).toBe(0); + + // Verify members were added/updated + const score = await redis.zscore(key, "one"); + expect(score).toBe(1.5); + }); + + test("should add members with NX option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-nx-test"; + + // Add initial member + await redis.zadd(key, "1", "one"); + + // Try to add with NX (should fail since member exists) + const added1 = await redis.zadd(key, "NX", "2", "one"); + expect(added1).toBe(0); + + // Verify score wasn't updated + const score1 = await redis.zscore(key, "one"); + expect(score1).toBe(1); + + // Add new member with NX (should succeed) + const added2 = await redis.zadd(key, "NX", "2", "two"); + expect(added2).toBe(1); + + const score2 = await redis.zscore(key, "two"); + expect(score2).toBe(2); + }); + + test("should update members with XX option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-xx-test"; + + // Add initial member + await redis.zadd(key, "1", "one"); + + // Update with XX (should succeed) + const updated1 = await redis.zadd(key, "XX", "2", "one"); + expect(updated1).toBe(0); // No new members added + + // Verify score was updated + const score1 = await redis.zscore(key, "one"); + expect(score1).toBe(2); + + // Try to add new member with XX (should fail) + const added = await redis.zadd(key, "XX", "3", "three"); + expect(added).toBe(0); + + // Verify member wasn't added + const score2 = await redis.zscore(key, "three"); + expect(score2).toBeNull(); + }); + + test("should return changed count with CH option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-ch-test"; + + // Add initial members + await redis.zadd(key, "1", "one", "2", "two"); + + // Add and update with CH (should return total changed count) + const changed = await redis.zadd(key, "CH", "1.5", "one", "3", "three"); + expect(changed).toBe(2); // one updated, three added + }); + + test("should increment score with INCR option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-incr-test"; + + // Add initial member + await redis.zadd(key, "1", "one"); + + // Increment with INCR option (returns new score as string) + const newScore = await redis.zadd(key, "INCR", "2.5", "one"); + expect(newScore).toBe(3.5); + + // Verify the score + const score = await redis.zscore(key, "one"); + expect(score).toBe(3.5); + }); + + test("should handle GT option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-gt-test"; + + // Add initial member + await redis.zadd(key, "5", "one"); + + // Try to update with lower score and GT (should fail) + const updated1 = await redis.zadd(key, "GT", "3", "one"); + expect(updated1).toBe(0); + + // Verify score wasn't updated + const score1 = await redis.zscore(key, "one"); + expect(score1).toBe(5); + + // Update with higher score and GT (should succeed) + const updated2 = await redis.zadd(key, "GT", "7", "one"); + expect(updated2).toBe(0); // No new members added + + // Verify score was updated + const score2 = await redis.zscore(key, "one"); + expect(score2).toBe(7); + }); + + test("should handle LT option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-lt-test"; + + // Add initial member + await redis.zadd(key, "5", "one"); + + // Try to update with higher score and LT (should fail) + const updated1 = await redis.zadd(key, "LT", "7", "one"); + expect(updated1).toBe(0); + + // Verify score wasn't updated + const score1 = await redis.zscore(key, "one"); + expect(score1).toBe(5); + + // Update with lower score and LT (should succeed) + const updated2 = await redis.zadd(key, "LT", "3", "one"); + expect(updated2).toBe(0); // No new members added + + // Verify score was updated + const score2 = await redis.zscore(key, "one"); + expect(score2).toBe(3); + }); + + test("should iterate sorted set with ZSCAN", async () => { + const redis = ctx.redis; + const key = "zscan-test"; + + // Add members + await redis.zadd(key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"); + + // Scan all elements + let cursor = "0"; + const allElements: string[] = []; + do { + const [nextCursor, elements] = await redis.zscan(key, cursor); + allElements.push(...elements); + cursor = nextCursor; + } while (cursor !== "0"); + + // Should have member-score pairs (10 elements total: 5 members + 5 scores) + expect(allElements.length).toBe(10); + + // Verify we got all members (check every other element for member names) + const members = allElements.filter((_, index) => index % 2 === 0); + expect(members).toContain("one"); + expect(members).toContain("two"); + expect(members).toContain("three"); + expect(members).toContain("four"); + expect(members).toContain("five"); + }); + + test("should iterate sorted set with ZSCAN and MATCH", async () => { + const redis = ctx.redis; + const key = "zscan-match-test"; + + // Add members with different patterns + await redis.zadd(key, "1", "user:1", "2", "user:2", "3", "post:1", "4", "post:2"); + + // Scan with MATCH pattern + let cursor = "0"; + const userElements: string[] = []; + do { + const [nextCursor, elements] = await redis.zscan(key, cursor, "MATCH", "user:*"); + userElements.push(...elements); + cursor = nextCursor; + } while (cursor !== "0"); + + // Extract member names (every other element) + const members = userElements.filter((_, index) => index % 2 === 0); + + // Should only find user keys + expect(members).toContain("user:1"); + expect(members).toContain("user:2"); + expect(members).not.toContain("post:1"); + expect(members).not.toContain("post:2"); + }); + + test("should iterate sorted set with ZSCAN and COUNT", async () => { + const redis = ctx.redis; + const key = "zscan-count-test"; + + // Add many members + const promises: Promise[] = []; + for (let i = 0; i < 100; i++) { + promises.push(redis.zadd(key, String(i), `member:${i}`)); + } + await Promise.all(promises); + + // Scan with COUNT hint + const [cursor, elements] = await redis.zscan(key, "0", "COUNT", "10"); + + expect(cursor).toBe("0"); // Should complete in one scan since we know the size + // COUNT is a hint, so we might get more or fewer elements + // Just verify we got some elements back + expect(elements.length).toBeGreaterThan(0); + expect(Array.isArray(elements)).toBe(true); + + const [cursor2, elements2] = await redis.zscan(key, 0, "COUNT", "10"); + expect(cursor2).toBe("0"); + expect(elements2.length).toBeGreaterThan(0); + expect(Array.isArray(elements2)).toBe(true); + }); + + test("should reject invalid key in ZADD", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zadd({} as any, "1", "member"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zadd'."`); + }); + + test("should reject invalid key in ZSCAN", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zscan([] as any, 0); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zscan'."`); + }); + + test("should return range of members with ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-basic-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get all members + const all = await redis.zrange(key, 0, -1); + expect(all).toEqual(["one", "two", "three", "four", "five"]); + + // Get first 3 members + const first3 = await redis.zrange(key, 0, 2); + expect(first3).toEqual(["one", "two", "three"]); + + // Get last 2 members using negative indices + const last2 = await redis.zrange(key, -2, -1); + expect(last2).toEqual(["four", "five"]); + }); + + test("should return members with scores using WITHSCORES option in ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-withscores-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2.5", "two", "3", "three"]); + + // Get members with scores + const result = await redis.zrange(key, 0, -1, "WITHSCORES"); + expect(result).toEqual([ + ["one", 1], + ["two", 2.5], + ["three", 3], + ]); + }); + + test("should return members by score range with BYSCORE option in ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-byscore-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get members with score 2-4 (inclusive) + const range1 = await redis.zrange(key, "2", "4", "BYSCORE"); + expect(range1).toEqual(["two", "three", "four"]); + + // Get members with score > 2 and <= 4 (exclusive start) + const range2 = await redis.zrange(key, "(2", "4", "BYSCORE"); + expect(range2).toEqual(["three", "four"]); + + // Get all members using infinity + const all = await redis.zrange(key, "-inf", "+inf", "BYSCORE"); + expect(all).toEqual(["one", "two", "three", "four", "five"]); + }); + + test("should return members in reverse order with REV option in ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-rev-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); + + // Get members in reverse order + const reversed = await redis.zrange(key, 0, -1, "REV"); + expect(reversed).toEqual(["three", "two", "one"]); + + // Get top 2 with highest scores + const top2 = await redis.zrange(key, 0, 1, "REV"); + expect(top2).toEqual(["three", "two"]); + }); + + test("should support LIMIT option with BYSCORE in ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-limit-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get members with score >= 1, limit 2 starting from offset 1 + const result = await redis.zrange(key, "1", "5", "BYSCORE", "LIMIT", "1", "2"); + expect(result).toEqual(["two", "three"]); + }); + + test("should return members by lexicographical range with BYLEX option in ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-bylex-test"; + + // Add members with same score (required for lex operations) + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date"]); + + // Get range from "banana" to "cherry" (inclusive) + const range1 = await redis.zrange(key, "[banana", "[cherry", "BYLEX"); + expect(range1).toEqual(["banana", "cherry"]); + + // Get range with exclusive bounds + const range2 = await redis.zrange(key, "(banana", "(date", "BYLEX"); + expect(range2).toEqual(["cherry"]); + }); + + test("should return members in reverse order with ZREVRANGE", async () => { + const redis = ctx.redis; + const key = "zrevrange-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get all members in reverse order + const all = await redis.zrevrange(key, 0, -1); + expect(all).toEqual(["five", "four", "three", "two", "one"]); + + // Get top 3 members with highest scores + const top3 = await redis.zrevrange(key, 0, 2); + expect(top3).toEqual(["five", "four", "three"]); + + // Get members using negative indices + const last2 = await redis.zrevrange(key, -2, -1); + expect(last2).toEqual(["two", "one"]); + }); + + test("should return members with scores using WITHSCORES option in ZREVRANGE", async () => { + const redis = ctx.redis; + const key = "zrevrange-withscores-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1.5", "one", "2", "two", "3.7", "three"]); + + // Get members with scores in reverse order + const result = await redis.zrevrange(key, 0, -1, "WITHSCORES"); + expect(result).toEqual([ + ["three", 3.7], + ["two", 2], + ["one", 1.5], + ]); + }); + + test("should handle empty sorted set with ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-empty-test"; + + // Query empty set + const result = await redis.zrange(key, 0, -1); + expect(result).toEqual([]); + }); + + test("should handle empty sorted set with ZREVRANGE", async () => { + const redis = ctx.redis; + const key = "zrevrange-empty-test"; + + // Query empty set + const result = await redis.zrevrange(key, 0, -1); + expect(result).toEqual([]); + }); + + test("should reject invalid key in ZRANGE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrange({} as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zrange'."`); + }); + + test("should reject invalid key in ZREVRANGE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrevrange([] as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrevrange'."`, + ); + }); + test("should return members by score range with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get all members + const all = await redis.zrangebyscore(key, "-inf", "+inf"); + expect(all).toEqual(["one", "two", "three", "four", "five"]); + + // Get members with score 2-4 (inclusive) + const range1 = await redis.zrangebyscore(key, 2, 4); + expect(range1).toEqual(["two", "three", "four"]); + + // Get members with exclusive lower bound + const range2 = await redis.zrangebyscore(key, "(2", 4); + expect(range2).toEqual(["three", "four"]); + + // Get members with exclusive upper bound + const range3 = await redis.zrangebyscore(key, 2, "(4"); + expect(range3).toEqual(["two", "three"]); + + // Get members with both exclusive bounds + const range4 = await redis.zrangebyscore(key, "(2", "(4"); + expect(range4).toEqual(["three"]); + }); + + test("should support WITHSCORES option with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-withscores-test"; + + // Add members + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + // Get with scores + const result = await redis.zrangebyscore(key, 1, 3, "WITHSCORES"); + expect(result).toEqual([ + ["one", 1.5], + ["two", 2.7], + ]); + }); + + test("should support LIMIT option with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-limit-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get first 2 members in score range + const limited1 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 0, 2); + expect(limited1).toEqual(["one", "two"]); + + // Skip first, get next 2 + const limited2 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 1, 2); + expect(limited2).toEqual(["two", "three"]); + + // Can combine with score range + const limited3 = await redis.zrangebyscore(key, 2, 5, "LIMIT", 1, 2); + expect(limited3).toEqual(["three", "four"]); + }); + + test("should support WITHSCORES with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-withscores-only-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); + + const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES"); + expect(result).toEqual([ + ["one", 1], + ["two", 2], + ["three", 3], + ["four", 4], + ]); + }); + + test("should support LIMIT and WITHSCORES together with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-combined-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); + + // Get with both options + const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES", "LIMIT", 1, 2); + expect(result).toEqual([ + ["two", 2], + ["three", 3], + ]); + }); + + test("should return members by score range in reverse with ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrevrangebyscore-test"; + + // Add members with scores (note: max comes before min in ZREVRANGEBYSCORE) + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get all members in reverse order + const all = await redis.zrevrangebyscore(key, "+inf", "-inf"); + expect(all).toEqual(["five", "four", "three", "two", "one"]); + + // Get members with score 4-2 (note: max=4, min=2) + const range1 = await redis.zrevrangebyscore(key, 4, 2); + expect(range1).toEqual(["four", "three", "two"]); + + // Get with exclusive bounds + const range2 = await redis.zrevrangebyscore(key, "(4", "(2"); + expect(range2).toEqual(["three"]); + + // Get with one exclusive bound + const range3 = await redis.zrevrangebyscore(key, 4, "(2"); + expect(range3).toEqual(["four", "three"]); + }); + + test("should support WITHSCORES option with ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrevrangebyscore-withscores-test"; + + // Add members + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + // Get with scores (max=3, min=1) + const result = await redis.zrevrangebyscore(key, 3, 1, "WITHSCORES"); + expect(result).toEqual([ + ["two", 2.7], + ["one", 1.5], + ]); + }); + + test("should support LIMIT option with ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrevrangebyscore-limit-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get first 2 members in reverse + const limited1 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 0, 2); + expect(limited1).toEqual(["five", "four"]); + + // Skip first, get next 2 + const limited2 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 1, 2); + expect(limited2).toEqual(["four", "three"]); + }); + + test("should return members by lexicographical range with ZRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zrangebylex-test"; + + // Add members with same score (required for lex operations) + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + + // Get all members + const all = await redis.zrangebylex(key, "-", "+"); + expect(all).toEqual(["apple", "banana", "cherry", "date", "elderberry"]); + + // Get range from "banana" to "date" (inclusive) + const range1 = await redis.zrangebylex(key, "[banana", "[date"); + expect(range1).toEqual(["banana", "cherry", "date"]); + + // Get range with exclusive bounds + const range2 = await redis.zrangebylex(key, "(banana", "(date"); + expect(range2).toEqual(["cherry"]); + + // Get range with one exclusive, one inclusive + const range3 = await redis.zrangebylex(key, "[banana", "(date"); + expect(range3).toEqual(["banana", "cherry"]); + + // Get range from start to specific member + const range4 = await redis.zrangebylex(key, "-", "[cherry"); + expect(range4).toEqual(["apple", "banana", "cherry"]); + + // Get range from specific member to end + const range5 = await redis.zrangebylex(key, "[cherry", "+"); + expect(range5).toEqual(["cherry", "date", "elderberry"]); + }); + + test("should support LIMIT option with ZRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zrangebylex-limit-test"; + + // Add members with same score + await redis.send("ZADD", [key, "0", "a", "0", "b", "0", "c", "0", "d", "0", "e", "0", "f", "0", "g"]); + + // Get first 3 members + const limited1 = await redis.zrangebylex(key, "-", "+", "LIMIT", 0, 3); + expect(limited1).toEqual(["a", "b", "c"]); + + // Skip first 2, get next 3 + const limited2 = await redis.zrangebylex(key, "-", "+", "LIMIT", 2, 3); + expect(limited2).toEqual(["c", "d", "e"]); + + // Get last 2 with large offset + const limited3 = await redis.zrangebylex(key, "-", "+", "LIMIT", 5, 10); + expect(limited3).toEqual(["f", "g"]); + }); + + test("should reject invalid key in ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrangebyscore({} as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrangebyscore'."`, + ); + }); + + test("should reject invalid key in ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrevrangebyscore([] as any, 10, 0); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrevrangebyscore'."`, + ); + }); + + test("should reject invalid key in ZRANGEBYLEX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrangebylex(null as any, "-", "+"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrangebylex'."`, + ); + }); + + test("should return members in reverse lexicographical order with ZREVRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zrevrangebylex-test"; + + // Add members with same score (required for lex operations) + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + + // Get all members in reverse order + const all = await redis.zrevrangebylex(key, "+", "-"); + expect(all).toEqual(["elderberry", "date", "cherry", "banana", "apple"]); + + // Get range from "date" to "banana" (inclusive, reverse order) + const range1 = await redis.zrevrangebylex(key, "[date", "[banana"); + expect(range1).toEqual(["date", "cherry", "banana"]); + + // Get range with exclusive bounds + const range2 = await redis.zrevrangebylex(key, "(date", "(banana"); + expect(range2).toEqual(["cherry"]); + + // Get range with one exclusive, one inclusive + const range3 = await redis.zrevrangebylex(key, "[elderberry", "(cherry"); + expect(range3).toEqual(["elderberry", "date"]); + }); + + test("should support LIMIT option with ZREVRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zrevrangebylex-limit-test"; + + // Add members with same score + await redis.send("ZADD", [key, "0", "a", "0", "b", "0", "c", "0", "d", "0", "e", "0", "f", "0", "g"]); + + // Get first 3 members in reverse order + const limited1 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "0", "3"); + expect(limited1).toEqual(["g", "f", "e"]); + + // Skip first 2, get next 3 + const limited2 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "2", "3"); + expect(limited2).toEqual(["e", "d", "c"]); + + // Get last 2 with large offset + const limited3 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "5", "10"); + expect(limited3).toEqual(["b", "a"]); + }); + + test("should store range of members with ZRANGESTORE", async () => { + const redis = ctx.redis; + const source = "zrangestore-source"; + const dest = "zrangestore-dest"; + + // Add members with scores + await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Store members by rank (index 1 to 3) + const count1 = await redis.zrangestore(dest, source, 1, 3); + expect(count1).toBe(3); + + // Verify stored members + const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); + expect(stored).toEqual(["two", "three", "four"]); + }); + + test("should store range with BYSCORE option in ZRANGESTORE", async () => { + const redis = ctx.redis; + const source = "zrangestore-byscore-source"; + const dest = "zrangestore-byscore-dest"; + + // Add members + await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Store members with score 2-4 (inclusive) + const count = await redis.zrangestore(dest, source, "2", "4", "BYSCORE"); + expect(count).toBe(3); + + // Verify stored members + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["two", 2], + ["three", 3], + ["four", 4], + ]); + }); + + test("should store range in reverse order with REV option in ZRANGESTORE", async () => { + const redis = ctx.redis; + const source = "zrangestore-rev-source"; + const dest = "zrangestore-rev-dest"; + + // Add members + await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three"]); + + // Store in reverse order + const count = await redis.zrangestore(dest, source, "0", "-1", "REV"); + expect(count).toBe(3); + + // Verify stored members (they maintain their scores but were selected in reverse) + const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); + expect(stored).toEqual(["one", "two", "three"]); + }); + + test("should support LIMIT option with ZRANGESTORE", async () => { + const redis = ctx.redis; + const source = "zrangestore-limit-source"; + const dest = "zrangestore-limit-dest"; + + // Add members + await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Store with BYSCORE and LIMIT + const count = await redis.zrangestore(dest, source, "-inf", "+inf", "BYSCORE", "LIMIT", "1", "2"); + expect(count).toBe(2); + + // Verify stored members (skip first, get next 2) + const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); + expect(stored).toEqual(["two", "three"]); + }); + + test("should reject invalid key in ZREVRANGEBYLEX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrevrangebylex({} as any, "+", "-"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrevrangebylex'."`, + ); + }); + + test("should reject invalid destination in ZRANGESTORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrangestore([] as any, "source", 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrangestore'."`, + ); + }); + + test("should reject invalid source in ZRANGESTORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrangestore("dest", null as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrangestore'."`, + ); }); }); From 17f43fd11f0e79c34332e95fa2babd6ce38fb0db Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 21:24:57 -0700 Subject: [PATCH 09/45] rm --- REDIS-COMMANDS-PLAN.md | 287 ----------------------------------------- redis.md | 127 ------------------ 2 files changed, 414 deletions(-) delete mode 100644 REDIS-COMMANDS-PLAN.md delete mode 100644 redis.md diff --git a/REDIS-COMMANDS-PLAN.md b/REDIS-COMMANDS-PLAN.md deleted file mode 100644 index fb05496e9481..000000000000 --- a/REDIS-COMMANDS-PLAN.md +++ /dev/null @@ -1,287 +0,0 @@ -# Redis Commands Implementation Plan - -## Current Status - -**Coverage: 13.2%** -- Total Redis commands: 486 -- Implemented: 64 -- Missing: 422 - -## Implemented Commands (64) - -The following commands are already implemented: - -### String Operations -APPEND, BITCOUNT, DECR, DEL, DUMP, EXISTS, EXPIRE, EXPIRETIME, GET, GETDEL, GETEX, GETSET, INCR, MGET, PERSIST, PEXPIRETIME, PTTL, SET, SETNX, STRLEN, TTL - -### Hash Operations -HGET, HGETALL, HINCRBY, HINCRBYFLOAT, HKEYS, HLEN, HMGET, HMSET, HSTRLEN, HVALS - -### List Operations -LLEN, LPOP, LPUSH, LPUSHX, RPOP, RPUSH, RPUSHX - -### Set Operations -SADD, SCARD, SISMEMBER, SMEMBERS, SMOVE, SPOP, SRANDMEMBER, SREM - -### Sorted Set Operations -ZCARD, ZPOPMAX, ZPOPMIN, ZRANDMEMBER, ZRANK, ZREVRANK, ZSCORE - -### Pub/Sub -PING, PSUBSCRIBE, PUBLISH, PUNSUBSCRIBE, SPUBLISH, SUBSCRIBE, UNSUBSCRIBE - -### Other -KEYS, PFADD, SCRIPT, SELECT, SUBSTR - -## Priority 1: Core Commands (106 commands) - -These are essential for most Redis use cases and should be implemented first. - -### 1. Core String Operations (11 commands) -Essential for basic Redis usage: -- `GETRANGE` - Get substring of string value -- `SETRANGE` - Overwrite part of string -- `SETEX` - Set key with expiration (seconds) -- `PSETEX` - Set key with expiration (milliseconds) -- `GETBIT` - Get bit value at offset -- `SETBIT` - Set bit value at offset -- `INCRBY` - Increment by integer value -- `INCRBYFLOAT` - Increment by float value -- `DECRBY` - Decrement by integer value -- `MSET` - Set multiple keys atomically -- `MSETNX` - Set multiple keys only if none exist - -**Implementation note**: Most of these follow simple patterns and can use the `compile.*` helpers. - -### 2. List Operations (15 commands) -Core list functionality: -- `LRANGE` - Get range of elements from list (very common!) -- `LINDEX` - Get element by index -- `LSET` - Set element at index -- `LINSERT` - Insert before/after pivot -- `LREM` - Remove elements -- `LTRIM` - Trim list to range -- `RPOPLPUSH` - Pop from one list, push to another -- `LPOS` - Find index of element -- `LMOVE` - Atomically move element between lists -- `BLPOP` - Blocking pop from left (important for queues!) -- `BRPOP` - Blocking pop from right -- `BLMOVE` - Blocking move -- `BRPOPLPUSH` - Blocking RPOPLPUSH -- `LMPOP` - Pop multiple elements -- `BLMPOP` - Blocking LMPOP - -**Implementation note**: Blocking operations (`BLPOP`, `BRPOP`) require special async handling. - -### 3. Set Operations (9 commands) -Set algebra operations: -- `SINTER` - Intersection of sets -- `SINTERSTORE` - Store intersection -- `SINTERCARD` - Get cardinality of intersection -- `SUNION` - Union of sets -- `SUNIONSTORE` - Store union -- `SDIFF` - Difference of sets -- `SDIFFSTORE` - Store difference -- `SMISMEMBER` - Check multiple members -- `SSCAN` - Iterate set members - -### 4. Sorted Set Operations (28 commands) -Most comprehensive missing category: -- `ZADD` - Add members with scores (essential!) -- `ZREM` - Remove members -- `ZINCRBY` - Increment member score -- `ZRANGE` - Get range by index (very common!) -- `ZREVRANGE` - Reverse range by index -- `ZRANGEBYSCORE` - Range by score -- `ZREVRANGEBYSCORE` - Reverse range by score -- `ZRANGEBYLEX` - Range by lexicographic order -- `ZREVRANGEBYLEX` - Reverse lexicographic range -- `ZCOUNT` - Count members in score range -- `ZLEXCOUNT` - Count in lexicographic range -- `ZREMRANGEBYRANK` - Remove by rank range -- `ZREMRANGEBYSCORE` - Remove by score range -- `ZREMRANGEBYLEX` - Remove by lexicographic range -- `ZRANGESTORE` - Store range result -- `ZINTER` - Intersection of sorted sets -- `ZINTERSTORE` - Store intersection -- `ZINTERCARD` - Intersection cardinality -- `ZDIFF` - Difference of sorted sets -- `ZDIFFSTORE` - Store difference -- `ZUNION` - Union of sorted sets -- `ZUNIONSTORE` - Store union -- `ZMSCORE` - Get scores of multiple members -- `ZSCAN` - Iterate sorted set -- `BZPOPMIN` - Blocking pop min -- `BZPOPMAX` - Blocking pop max -- `ZMPOP` - Pop multiple elements -- `BZMPOP` - Blocking ZMPOP - -### 5. Hash Operations (18 commands) -Additional hash functionality: -- `HSET` - Set field value (essential!) -- `HSETNX` - Set field only if not exists -- `HDEL` - Delete fields -- `HEXISTS` - Check field exists -- `HRANDFIELD` - Get random field -- `HSCAN` - Iterate hash fields -- Plus newer hash expiration commands: `HGETDEL`, `HGETEX`, `HSETEX`, `HEXPIRE`, `HEXPIREAT`, `HEXPIRETIME`, `HPERSIST`, `HPEXPIRE`, `HPEXPIREAT`, `HPEXPIRETIME`, `HPTTL`, `HTTL` - -### 6. Key Expiration & TTL (3 commands) -- `EXPIREAT` - Set expiration timestamp (seconds) -- `PEXPIRE` - Set expiration (milliseconds) -- `PEXPIREAT` - Set expiration timestamp (milliseconds) - -### 7. Key Management (8 commands) -- `RENAME` - Rename key -- `RENAMENX` - Rename only if new key doesn't exist -- `COPY` - Copy key to new key -- `UNLINK` - Async delete (better than DEL for large keys) -- `TYPE` - Get type of key -- `TOUCH` - Update last access time -- `RANDOMKEY` - Get random key -- `SCAN` - Iterate keyspace (important for production!) - -### 8. Transactions (5 commands) -Critical for atomicity: -- `MULTI` - Start transaction -- `EXEC` - Execute transaction -- `DISCARD` - Discard transaction -- `WATCH` - Watch keys for changes -- `UNWATCH` - Unwatch keys - -**Implementation note**: Requires transaction state management in the client. - -### 9. Scripting (9 commands) -Lua script support: -- `EVAL` - Execute Lua script -- `EVALSHA` - Execute by SHA1 -- `EVAL_RO` - Read-only eval -- `EVALSHA_RO` - Read-only evalsha -- `SCRIPT LOAD` - Load script, return SHA1 -- `SCRIPT EXISTS` - Check if scripts exist -- `SCRIPT FLUSH` - Remove all scripts -- `SCRIPT KILL` - Kill running script -- `SCRIPT DEBUG` - Set debug mode - -## Priority 2: Extended Features (73 commands) - -### HyperLogLog (2) -- `PFCOUNT`, `PFMERGE` - -### Bit Operations (4) -- `BITOP`, `BITPOS`, `BITFIELD`, `BITFIELD_RO` - -### Geo Commands (10) -Full geospatial support for location-based features - -### Streams (21) -Redis Streams for message broker functionality - becoming increasingly popular - -### Server Management (15) -`INFO`, `DBSIZE`, `TIME`, `ECHO`, `CONFIG GET/SET`, `FLUSHDB`, `FLUSHALL`, etc. - -### Client Management (11) -Connection management commands - -### Pub/Sub Advanced (7) -Extended pub/sub introspection - -### Sorting & Searching (3) -`SORT`, `SORT_RO`, `LCS` - -## Priority 3: Optional/Advanced (104+ commands) - -These can be implemented later or as needed: -- Functions (Redis 7.0+) -- Cluster management -- ACL (Access Control Lists) -- Replication commands -- Memory diagnostics -- Module extensions (RedisJSON, RediSearch, Bloom filters, etc.) - -## Implementation Strategy - -### Phase 1: Quick Wins (Estimated: 1-2 days) -Implement simple commands using the `compile.*` helpers: - -```typescript -// In valkey.classes.ts, add to proto: -getrange: { fn: "getrange" }, -setrange: { fn: "setrange" }, -incrby: { fn: "incrby" }, -// ... etc -``` - -```zig -// In js_valkey_functions.zig: -pub const getrange = compile.@"(key: RedisKey, start: number, end: number)"("getrange", "GETRANGE", .not_subscriber).call; -pub const setrange = compile.@"(key: RedisKey, offset: number, value: RedisValue)"("setrange", "SETRANGE", .not_subscriber).call; -pub const incrby = compile.@"(key: RedisKey, increment: number)"("incrby", "INCRBY", .not_subscriber).call; -``` - -**Target**: Core String Operations (11), Key Expiration (3), Key Management (8) = 22 commands - -### Phase 2: List Operations (Estimated: 2-3 days) -Implement all list commands including blocking operations. - -**Target**: 15 list commands - -### Phase 3: Set & Hash Operations (Estimated: 2-3 days) -Complete set operations and hash commands. - -**Target**: 9 set + 18 hash = 27 commands - -### Phase 4: Sorted Sets (Estimated: 3-4 days) -This is the largest category and needs careful implementation. - -**Target**: 28 sorted set commands - -### Phase 5: Transactions & Scripting (Estimated: 3-4 days) -Requires special state management. - -**Target**: 5 transaction + 9 scripting = 14 commands - -### After Phase 5 -You'll have implemented **106 high-priority commands**, bringing total coverage to: -- **170 commands implemented (35% coverage)** - -## Testing Strategy - -For each command implemented: -1. Create test in `test/js/bun/redis/` -2. Test basic functionality -3. Test edge cases (nil values, wrong types, etc.) -4. Test error handling -5. Use snapshot testing where appropriate - -Example test structure: -```typescript -import { test, expect } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; - -test("LRANGE returns list elements", async () => { - const redis = new RedisClient(); - await redis.connect(); - - await redis.lpush("mylist", "world"); - await redis.lpush("mylist", "hello"); - - const result = await redis.lrange("mylist", 0, -1); - expect(result).toEqual(["hello", "world"]); - - redis.close(); -}); -``` - -## Notes - -- Many commands can share implementation patterns via the `compile.*` helpers -- Blocking commands (`BLPOP`, `BRPOP`, etc.) need special async handling -- Transaction support requires maintaining transaction state -- Some module commands (JSON, Search, etc.) may not be needed immediately -- Focus on commonly-used commands first for maximum impact - -## References - -- Redis Commands: https://redis.io/commands -- Current implementation: `src/bun.js/api/valkey.classes.ts` -- Command functions: `src/valkey/js_valkey_functions.zig` \ No newline at end of file diff --git a/redis.md b/redis.md deleted file mode 100644 index c4850781a5e3..000000000000 --- a/redis.md +++ /dev/null @@ -1,127 +0,0 @@ -# Redis Commands Implementation Progress - -## Core String Operations (11 commands) - -- [x] GETRANGE -- [x] SETRANGE -- [x] SETEX -- [x] PSETEX -- [x] GETBIT -- [x] SETBIT -- [x] INCRBY -- [x] INCRBYFLOAT -- [x] DECRBY -- [x] MSET -- [x] MSETNX - -## List Operations (15 commands) - -- [ ] LRANGE -- [ ] LINDEX -- [ ] LSET -- [ ] LINSERT -- [ ] LREM -- [ ] LTRIM -- [ ] RPOPLPUSH -- [ ] LPOS -- [ ] LMOVE -- [ ] BLPOP -- [ ] BRPOP -- [ ] BLMOVE -- [ ] BRPOPLPUSH -- [ ] LMPOP -- [ ] BLMPOP - -## Set Operations (9 commands) - -- [ ] SINTER -- [ ] SINTERSTORE -- [ ] SINTERCARD -- [ ] SUNION -- [ ] SUNIONSTORE -- [ ] SDIFF -- [ ] SDIFFSTORE -- [ ] SMISMEMBER -- [ ] SSCAN - -## Sorted Set Operations (28 commands) - -### Tier 1 - Simple (6 commands) -- [x] ZINCRBY -- [x] ZCOUNT -- [x] ZLEXCOUNT -- [x] ZREMRANGEBYRANK -- [x] ZREMRANGEBYSCORE -- [x] ZREMRANGEBYLEX - -### Tier 2 - Simple Variadic (2 commands) -- [x] ZREM -- [x] ZMSCORE - -### Tier 3 - Complex Options (9 commands) -- [x] ZADD -- [x] ZRANGE -- [x] ZREVRANGE -- [x] ZRANGEBYSCORE -- [x] ZREVRANGEBYSCORE -- [x] ZRANGEBYLEX -- [x] ZREVRANGEBYLEX -- [x] ZRANGESTORE -- [x] ZSCAN - -### Tier 4 - Set Operations (7 commands) -- [ ] ZINTER -- [ ] ZINTERSTORE -- [ ] ZINTERCARD -- [ ] ZDIFF -- [ ] ZDIFFSTORE -- [ ] ZUNION -- [ ] ZUNIONSTORE - -### Tier 5 - Blocking (4 commands) -- [ ] BZPOPMIN -- [ ] BZPOPMAX -- [ ] ZMPOP -- [ ] BZMPOP - -## Hash Operations (18 commands) - -- [ ] HSET -- [ ] HSETNX -- [ ] HDEL -- [ ] HEXISTS -- [ ] HRANDFIELD -- [ ] HSCAN -- [ ] HGETDEL -- [ ] HGETEX -- [ ] HSETEX -- [ ] HEXPIRE -- [ ] HEXPIREAT -- [ ] HEXPIRETIME -- [ ] HPERSIST -- [ ] HPEXPIRE -- [ ] HPEXPIREAT -- [ ] HPEXPIRETIME -- [ ] HPTTL -- [ ] HTTL - -## Key Management (8 commands) - -- [x] RENAME -- [x] RENAMENX -- [x] COPY -- [x] UNLINK -- [x] TYPE -- [x] TOUCH -- [x] RANDOMKEY -- [x] SCAN - -## Key Expiration & TTL (3 commands) - -- [x] EXPIREAT -- [x] PEXPIRE -- [x] PEXPIREAT - ---- - -**Total: 92 commands** \ No newline at end of file From 3694529f6d8477f364fb9cfda7f36b14a050fec4 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 21:50:35 -0700 Subject: [PATCH 10/45] feat: Implement some more set operations --- packages/bun-types/redis.d.ts | 346 +++++++++++++++++ src/bun.js/api/valkey.classes.ts | 28 ++ src/valkey/js_valkey.zig | 7 + src/valkey/js_valkey_functions.zig | 7 + test/js/valkey/valkey.test.ts | 580 ++++++++++++++++++++++++++++- 5 files changed, 964 insertions(+), 4 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index a271c847429c..00b366deb3e7 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -1921,6 +1921,352 @@ declare module "bun" { * ``` */ renamenx(key: RedisClient.KeyLike, newkey: RedisClient.KeyLike): Promise; + + /** + * Compute the difference between sorted sets with scores + * + * @param numkeys The number of sorted set keys + * @param keys The sorted set keys followed by "WITHSCORES" + * @returns Promise that resolves with an array of [member, score] pairs + * + * @example + * ```ts + * await redis.send("ZADD", ["zset1", "1", "one", "2", "two", "3", "three"]); + * await redis.send("ZADD", ["zset2", "1", "one", "2", "two"]); + * const diff = await redis.zdiff(2, "zset1", "zset2", "WITHSCORES"); + * console.log(diff); // ["three", "3"] + * ``` + */ + zdiff( + numkeys: number, + ...args: [...keys: RedisClient.KeyLike[], withscores: "WITHSCORES"] + ): Promise<[string, number][]>; + + /** + * Compute the difference between the first sorted set and all successive sorted sets + * + * Returns the members of the sorted set resulting from the difference between the first + * sorted set and all the successive sorted sets. The first key is the only one used to + * compute the members of the difference. + * + * @param numkeys The number of sorted set keys + * @param keys The sorted set keys to compare + * @returns Promise that resolves with an array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["zset1", "1", "one", "2", "two", "3", "three"]); + * await redis.send("ZADD", ["zset2", "1", "one", "2", "two"]); + * const diff = await redis.zdiff(2, "zset1", "zset2"); + * console.log(diff); // ["three"] + * ``` + */ + zdiff(numkeys: number, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Compute the difference between sorted sets and store the result + * + * Computes the difference between the first and all successive sorted sets given by the + * specified keys and stores the result in destination. Keys that do not exist are + * considered to be empty sets. + * + * @param destination The destination key to store the result + * @param numkeys The number of input sorted set keys + * @param keys The sorted set keys to compare + * @returns Promise that resolves with the number of elements in the resulting sorted set + * + * @example + * ```ts + * await redis.send("ZADD", ["zset1", "1", "one", "2", "two", "3", "three"]); + * await redis.send("ZADD", ["zset2", "1", "one"]); + * const count = await redis.zdiffstore("out", 2, "zset1", "zset2"); + * console.log(count); // 2 (two, three) + * ``` + */ + zdiffstore(destination: RedisClient.KeyLike, numkeys: number, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Compute the intersection of multiple sorted sets + * + * Returns the members of the set resulting from the intersection of all the given sorted sets. + * Keys that do not exist are considered to be empty sets. + * + * By default, the resulting score of each member is the sum of its scores in the sorted sets where it exists. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * - WITHSCORES: Return the scores along with the members + * + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to intersect + * @returns Promise that resolves with an array of members (or [member, score] pairs if WITHSCORES) + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "1", "b", "2", "c", "3", "d"); + * + * // Basic intersection - returns members that exist in all sets + * const result1 = await redis.zinter(2, "zset1", "zset2"); + * // Returns: ["b", "c"] + * + * // With scores (sum by default) + * const result2 = await redis.zinter(2, "zset1", "zset2", "WITHSCORES"); + * // Returns: ["b", "3", "c", "5"] (b: 2+1=3, c: 3+2=5) + * + * // With weights + * const result3 = await redis.zinter(2, "zset1", "zset2", "WEIGHTS", "2", "3", "WITHSCORES"); + * // Returns: ["b", "7", "c", "12"] (b: 2*2+1*3=7, c: 3*2+2*3=12) + * + * // With MIN aggregation + * const result4 = await redis.zinter(2, "zset1", "zset2", "AGGREGATE", "MIN", "WITHSCORES"); + * // Returns: ["b", "1", "c", "2"] (minimum scores) + * ``` + */ + zinter( + numkeys: number, + ...args: [...args: (string | number)[], withscores: "WITHSCORES"] + ): Promise<[string, number][]>; + + /** + * Compute the intersection of multiple sorted sets + * + * Returns the members of the set resulting from the intersection of all the given sorted sets. + * Keys that do not exist are considered to be empty sets. + * + * By default, the resulting score of each member is the sum of its scores in the sorted sets where it exists. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * - WITHSCORES: Return the scores along with the members + * + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to intersect + * @returns Promise that resolves with an array of members (or [member, score] pairs if WITHSCORES) + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "1", "b", "2", "c", "3", "d"); + * + * // Basic intersection - returns members that exist in all sets + * const result1 = await redis.zinter(2, "zset1", "zset2"); + * // Returns: ["b", "c"] + * + * // With scores (sum by default) + * const result2 = await redis.zinter(2, "zset1", "zset2", "WITHSCORES"); + * // Returns: ["b", "3", "c", "5"] (b: 2+1=3, c: 3+2=5) + * + * // With weights + * const result3 = await redis.zinter(2, "zset1", "zset2", "WEIGHTS", "2", "3", "WITHSCORES"); + * // Returns: ["b", "7", "c", "12"] (b: 2*2+1*3=7, c: 3*2+2*3=12) + * + * // With MIN aggregation + * const result4 = await redis.zinter(2, "zset1", "zset2", "AGGREGATE", "MIN", "WITHSCORES"); + * // Returns: ["b", "1", "c", "2"] (minimum scores) + * ``` + */ + zinter(numkeys: number, ...args: (string | number)[]): Promise; + + /** + * Count the number of members in the intersection of multiple sorted sets + * + * Computes the cardinality of the intersection of the sorted sets at the specified keys. + * The intersection includes only elements that exist in all of the given sorted sets. + * + * When a LIMIT is provided, the command stops counting once the limit is reached, which + * is useful for performance when you only need to know if the cardinality exceeds a + * certain threshold. + * + * @param numkeys The number of sorted set keys + * @param keys The sorted set keys to intersect + * @returns Promise that resolves with the number of elements in the intersection + * + * @example + * ```ts + * await redis.send("ZADD", ["zset1", "1", "one", "2", "two", "3", "three"]); + * await redis.send("ZADD", ["zset2", "1", "one", "2", "two", "4", "four"]); + * const count = await redis.zintercard(2, "zset1", "zset2"); + * console.log(count); // 2 (one, two) + * ``` + */ + zintercard(numkeys: number, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Count the number of members in the intersection with a limit + * + * @param numkeys The number of sorted set keys + * @param keys The sorted set keys followed by "LIMIT" and limit value + * @returns Promise that resolves with the number of elements (up to limit) + * + * @example + * ```ts + * await redis.send("ZADD", ["zset1", "1", "a", "2", "b", "3", "c"]); + * await redis.send("ZADD", ["zset2", "1", "a", "2", "b", "3", "c"]); + * const count = await redis.zintercard(2, "zset1", "zset2", "LIMIT", 2); + * console.log(count); // 2 (stopped at limit) + * ``` + */ + zintercard(numkeys: number, ...args: (RedisClient.KeyLike | "LIMIT" | number)[]): Promise; + + /** + * Compute the intersection of multiple sorted sets and store in destination + * + * This command is similar to ZINTER, but instead of returning the result, it stores it in the destination key. + * If the destination key already exists, it is overwritten. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * + * @param destination The destination key to store the result + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to intersect and optional WEIGHTS/AGGREGATE options + * @returns Promise that resolves with the number of elements in the resulting sorted set + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "1", "b", "2", "c", "3", "d"); + * + * // Basic intersection store + * const count1 = await redis.zinterstore("out", 2, "zset1", "zset2"); + * // Returns: 2 (stored "b" and "c" in "out") + * + * // With weights + * const count2 = await redis.zinterstore("out2", 2, "zset1", "zset2", "WEIGHTS", "2", "3"); + * // Returns: 2 + * + * // With MAX aggregation + * const count3 = await redis.zinterstore("out3", 2, "zset1", "zset2", "AGGREGATE", "MAX"); + * // Returns: 2 (stores maximum scores) + * ``` + */ + zinterstore(destination: RedisClient.KeyLike, numkeys: number, ...args: (string | number)[]): Promise; + + /** + * Compute the union of multiple sorted sets + * + * Returns the union of the sorted sets given by the specified keys. + * For every element that appears in at least one of the input sorted sets, the output will contain that element. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * - WITHSCORES: Include scores in the result + * + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to union and optional WEIGHTS/AGGREGATE/WITHSCORES options + * @returns Promise that resolves with an array of members (or members with scores if WITHSCORES is used) + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "4", "b", "5", "c", "6", "d"); + * + * // Basic union + * const members1 = await redis.zunion(2, "zset1", "zset2"); + * // Returns: ["a", "b", "c", "d"] + * + * // With weights + * const members2 = await redis.zunion(2, "zset1", "zset2", "WEIGHTS", "2", "3"); + * // Returns: ["a", "b", "c", "d"] with calculated scores + * + * // With MIN aggregation + * const members3 = await redis.zunion(2, "zset1", "zset2", "AGGREGATE", "MIN"); + * // Returns: ["a", "b", "c", "d"] with minimum scores + * + * // With scores + * const withScores = await redis.zunion(2, "zset1", "zset2", "WITHSCORES"); + * // Returns: ["a", "1", "b", "2", "c", "3", "d", "6"] (alternating member and score) + * ``` + */ + zunion( + numkeys: number, + ...args: [...args: (string | number)[], withscores: "WITHSCORES"] + ): Promise<[string, number][]>; + + /** + * Compute the union of multiple sorted sets + * + * Returns the union of the sorted sets given by the specified keys. + * For every element that appears in at least one of the input sorted sets, the output will contain that element. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * - WITHSCORES: Include scores in the result + * + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to union and optional WEIGHTS/AGGREGATE/WITHSCORES options + * @returns Promise that resolves with an array of members (or members with scores if WITHSCORES is used) + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "4", "b", "5", "c", "6", "d"); + * + * // Basic union + * const members1 = await redis.zunion(2, "zset1", "zset2"); + * // Returns: ["a", "b", "c", "d"] + * + * // With weights + * const members2 = await redis.zunion(2, "zset1", "zset2", "WEIGHTS", "2", "3"); + * // Returns: ["a", "b", "c", "d"] with calculated scores + * + * // With MIN aggregation + * const members3 = await redis.zunion(2, "zset1", "zset2", "AGGREGATE", "MIN"); + * // Returns: ["a", "b", "c", "d"] with minimum scores + * + * // With scores + * const withScores = await redis.zunion(2, "zset1", "zset2", "WITHSCORES"); + * // Returns: ["a", "1", "b", "2", "c", "3", "d", "6"] (alternating member and score) + * ``` + */ + zunion(numkeys: number, ...args: (string | number)[]): Promise; + + /** + * Compute the union of multiple sorted sets and store in destination + * + * This command is similar to ZUNION, but instead of returning the result, it stores it in the destination key. + * If the destination key already exists, it is overwritten. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * + * @param destination The destination key to store the result + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to union and optional WEIGHTS/AGGREGATE options + * @returns Promise that resolves with the number of elements in the resulting sorted set + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "4", "b", "5", "c", "6", "d"); + * + * // Basic union store + * const count1 = await redis.zunionstore("out", 2, "zset1", "zset2"); + * // Returns: 4 (stored "a", "b", "c", "d" in "out") + * + * // With weights + * const count2 = await redis.zunionstore("out2", 2, "zset1", "zset2", "WEIGHTS", "2", "3"); + * // Returns: 4 + * + * // With MAX aggregation + * const count3 = await redis.zunionstore("out3", 2, "zset1", "zset2", "AGGREGATE", "MAX"); + * // Returns: 4 (stores maximum scores) + * ``` + */ + zunionstore(destination: RedisClient.KeyLike, numkeys: number, ...args: (string | number)[]): Promise; } /** diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 7ae74834f27d..2e9bd9033f87 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -312,6 +312,34 @@ export default [ fn: "zscan", length: 2, }, + zdiff: { + fn: "zdiff", + length: 1, + }, + zdiffstore: { + fn: "zdiffstore", + length: 2, + }, + zinter: { + fn: "zinter", + length: 2, + }, + zintercard: { + fn: "zintercard", + length: 1, + }, + zinterstore: { + fn: "zinterstore", + length: 3, + }, + zunion: { + fn: "zunion", + length: 2, + }, + zunionstore: { + fn: "zunionstore", + length: 3, + }, mget: { fn: "mget", }, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 56e8b569888c..45ba5ca2d704 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1347,6 +1347,13 @@ pub const JSValkeyClient = struct { pub const zmscore = fns.zmscore; pub const zadd = fns.zadd; pub const zscan = fns.zscan; + pub const zdiff = fns.zdiff; + pub const zdiffstore = fns.zdiffstore; + pub const zinter = fns.zinter; + pub const zintercard = fns.zintercard; + pub const zinterstore = fns.zinterstore; + pub const zunion = fns.zunion; + pub const zunionstore = fns.zunionstore; const fns = @import("./js_valkey_functions.zig"); }; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index d07cd1ec432b..ccbfac54602b 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -690,6 +690,13 @@ pub const zincrby = compile.@"(...strings: string[])"("zincrby", "ZINCRBY", .not pub const zmscore = compile.@"(...strings: string[])"("zmscore", "ZMSCORE", .not_subscriber).call; pub const zadd = compile.@"(...strings: string[])"("zadd", "ZADD", .not_subscriber).call; pub const zscan = compile.@"(...strings: string[])"("zscan", "ZSCAN", .not_subscriber).call; +pub const zdiff = compile.@"(...strings: string[])"("zdiff", "ZDIFF", .not_subscriber).call; +pub const zdiffstore = compile.@"(...strings: string[])"("zdiffstore", "ZDIFFSTORE", .not_subscriber).call; +pub const zinter = compile.@"(...strings: string[])"("zinter", "ZINTER", .not_subscriber).call; +pub const zintercard = compile.@"(...strings: string[])"("zintercard", "ZINTERCARD", .not_subscriber).call; +pub const zinterstore = compile.@"(...strings: string[])"("zinterstore", "ZINTERSTORE", .not_subscriber).call; +pub const zunion = compile.@"(...strings: string[])"("zunion", "ZUNION", .not_subscriber).call; +pub const zunionstore = compile.@"(...strings: string[])"("zunionstore", "ZUNIONSTORE", .not_subscriber).call; pub const del = compile.@"(key: RedisKey, ...args: RedisKey[])"("del", "DEL", "key", .not_subscriber).call; pub const mget = compile.@"(key: RedisKey, ...args: RedisKey[])"("mget", "MGET", "key", .not_subscriber).call; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 6a1c44a64b02..fbe3858c8c55 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -366,10 +366,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const result = await redis.pexpire(key, 5000); expect(result).toBe(1); // 1 indicates success - // Verify TTL is set using PTTL (should be around 5000 ms) const pttl = await redis.pttl(key); expect(pttl).toBeGreaterThan(0); - expect(pttl).toBeLessThanOrEqual(5000); + expect(pttl).toBeLessThanOrEqual(5050); }); test("should return 0 for PEXPIRE on non-existent key", async () => { @@ -388,10 +387,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const result = await redis.pexpireat(key, futureTimestampMs); expect(result).toBe(1); // 1 indicates success - // Verify TTL is set using PTTL (should be around 5000 ms) const pttl = await redis.pttl(key); expect(pttl).toBeGreaterThan(0); - expect(pttl).toBeLessThanOrEqual(5000); + expect(pttl).toBeLessThanOrEqual(5050); }); test("should return 0 for PEXPIREAT on non-existent key", async () => { @@ -945,6 +943,133 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(count4).toBe(0); }); + test("should compute difference between sorted sets with ZDIFF", async () => { + const redis = ctx.redis; + const key1 = "zdiff-test1"; + const key2 = "zdiff-test2"; + const key3 = "zdiff-test3"; + + // Set up sorted sets + await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three", "4", "four"]); + await redis.send("ZADD", [key2, "1", "one", "2", "two"]); + await redis.send("ZADD", [key3, "3", "three"]); + + // Difference between first and second set + const diff1 = await redis.zdiff(2, key1, key2); + expect(diff1).toEqual(["three", "four"]); + + // Difference with multiple sets + const diff2 = await redis.zdiff(3, key1, key2, key3); + expect(diff2).toEqual(["four"]); + + // Difference with WITHSCORES + const diff3 = await redis.zdiff(2, key1, key2, "WITHSCORES"); + expect(diff3).toEqual([ + ["three", 3], + ["four", 4], + ]); + + // Difference with non-existent set (should return all from first set) + const diff4 = await redis.zdiff(2, key1, "nonexistent"); + expect(diff4.length).toBe(4); + expect(diff4).toEqual(["one", "two", "three", "four"]); + + // Empty result + const diff5 = await redis.zdiff(2, key2, key1); + expect(diff5).toEqual([]); + }); + + test("should store difference between sorted sets with ZDIFFSTORE", async () => { + const redis = ctx.redis; + const key1 = "zdiffstore-test1"; + const key2 = "zdiffstore-test2"; + const dest = "zdiffstore-dest"; + + // Set up sorted sets + await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three"]); + await redis.send("ZADD", [key2, "1", "one"]); + + // Store the difference + const count = await redis.zdiffstore(dest, 2, key1, key2); + expect(count).toBe(2); + + // Verify the destination has the correct members + const members = await redis.send("ZRANGE", [dest, "0", "-1"]); + expect(members).toEqual(["two", "three"]); + + // Verify scores are preserved + const membersWithScores = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(membersWithScores).toEqual([ + ["two", 2], + ["three", 3], + ]); + + // Store empty result (should overwrite existing key) + const count2 = await redis.zdiffstore(dest, 2, key2, key1); + expect(count2).toBe(0); + + // Verify destination is now empty + const finalCount = await redis.send("ZCARD", [dest]); + expect(finalCount).toBe(0); + }); + + test("should count intersection with ZINTERCARD", async () => { + const redis = ctx.redis; + const key1 = "zintercard-test1"; + const key2 = "zintercard-test2"; + const key3 = "zintercard-test3"; + + // Set up sorted sets + await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three"]); + await redis.send("ZADD", [key2, "1", "one", "2", "two", "4", "four"]); + await redis.send("ZADD", [key3, "1", "one", "5", "five"]); + + // Basic intersection count + const count1 = await redis.zintercard(2, key1, key2); + expect(count1).toBe(2); // one and two + + // Intersection of three sets + const count2 = await redis.zintercard(3, key1, key2, key3); + expect(count2).toBe(1); // only one + + // With LIMIT + const count3 = await redis.zintercard(2, key1, key2, "LIMIT", 1); + expect(count3).toBe(1); // stopped at limit + + // No intersection + const count4 = await redis.zintercard(2, key1, key3); + expect(count4).toBe(1); // only one exists in both + + // With non-existent set + const count5 = await redis.zintercard(2, key1, "nonexistent"); + expect(count5).toBe(0); + }); + + test("should reject invalid arguments in ZDIFF", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zdiff({} as any, "key1"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zdiff'."`); + }); + + test("should reject invalid arguments in ZDIFFSTORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zdiffstore("dest", {} as any, "key1"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zdiffstore'."`, + ); + }); + + test("should reject invalid arguments in ZINTERCARD", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zintercard({} as any, "key1"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zintercard'."`, + ); + }); + test("should remove members by rank with ZREMRANGEBYRANK", async () => { const redis = ctx.redis; const key = "zremrangebyrank-test"; @@ -1901,6 +2026,453 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { `"Expected additional arguments to be a string or buffer for 'zrangestore'."`, ); }); + + test("should compute intersection with ZINTER", async () => { + const redis = ctx.redis; + const key1 = "zinter-test-1"; + const key2 = "zinter-test-2"; + + // Set up sorted sets + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); + + // Basic intersection - returns members that exist in all sets + const result1 = await redis.zinter(2, key1, key2); + expect(result1).toEqual(["b", "c"]); + + // With WITHSCORES (scores are summed by default) + const result2 = await redis.zinter(2, key1, key2, "WITHSCORES"); + expect(result2).toEqual([ + ["b", 3], + ["c", 5], + ]); + }); + + test("should compute intersection with WEIGHTS in ZINTER", async () => { + const redis = ctx.redis; + const key1 = "zinter-weights-1"; + const key2 = "zinter-weights-2"; + + // Set up sorted sets + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); + + // With weights (multiply scores) + const result = await redis.zinter(2, key1, key2, "WEIGHTS", "2", "3", "WITHSCORES"); + expect(result).toEqual([ + ["b", 7], + ["c", 12], + ]); + }); + + test("should compute intersection with AGGREGATE in ZINTER", async () => { + const redis = ctx.redis; + const key1 = "zinter-agg-1"; + const key2 = "zinter-agg-2"; + + // Set up sorted sets + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); + + // With MIN aggregation + const result1 = await redis.zinter(2, key1, key2, "AGGREGATE", "MIN", "WITHSCORES"); + expect(result1).toEqual([ + ["b", 1], + ["c", 2], + ]); + + // With MAX aggregation + const result2 = await redis.zinter(2, key1, key2, "AGGREGATE", "MAX", "WITHSCORES"); + expect(result2).toEqual([ + ["b", 2], + ["c", 3], + ]); + }); + + test("should handle empty intersection with ZINTER", async () => { + const redis = ctx.redis; + const key1 = "zinter-empty-1"; + const key2 = "zinter-empty-2"; + + // Set up sorted sets with no common members + await redis.zadd(key1, "1", "a", "2", "b"); + await redis.zadd(key2, "1", "c", "2", "d"); + + // Empty intersection + const result = await redis.zinter(2, key1, key2); + expect(result).toEqual([]); + }); + + test("should store intersection with ZINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "zinterstore-test-1"; + const key2 = "zinterstore-test-2"; + const dest = "zinterstore-dest"; + + // Set up sorted sets + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); + + // Basic intersection store + const count = await redis.zinterstore(dest, 2, key1, key2); + expect(count).toBe(2); + + // Verify stored members + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["b", 3], + ["c", 5], + ]); + }); + + test("should store intersection with WEIGHTS in ZINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "zinterstore-weights-1"; + const key2 = "zinterstore-weights-2"; + const dest = "zinterstore-weights-dest"; + + // Set up sorted sets + await redis.zadd(key1, "1", "x", "2", "y"); + await redis.zadd(key2, "2", "x", "3", "y"); + + // With weights + const count = await redis.zinterstore(dest, 2, key1, key2, "WEIGHTS", "2", "3"); + expect(count).toBe(2); + + // Verify stored members with weighted scores + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["x", 8], + ["y", 13], + ]); + }); + + test("should store intersection with AGGREGATE in ZINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "zinterstore-agg-1"; + const key2 = "zinterstore-agg-2"; + const destMin = "zinterstore-agg-min"; + const destMax = "zinterstore-agg-max"; + + // Set up sorted sets + await redis.zadd(key1, "1", "m", "3", "n"); + await redis.zadd(key2, "2", "m", "1", "n"); + + // With MIN aggregation + const count1 = await redis.zinterstore(destMin, 2, key1, key2, "AGGREGATE", "MIN"); + expect(count1).toBe(2); + const storedMin = await redis.send("ZRANGE", [destMin, "0", "-1", "WITHSCORES"]); + expect(storedMin).toEqual([ + ["m", 1], + ["n", 1], + ]); + + // With MAX aggregation + const count2 = await redis.zinterstore(destMax, 2, key1, key2, "AGGREGATE", "MAX"); + expect(count2).toBe(2); + const storedMax = await redis.send("ZRANGE", [destMax, "0", "-1", "WITHSCORES"]); + expect(storedMax).toEqual([ + ["m", 2], + ["n", 3], + ]); + }); + + test("should handle empty result with ZINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "zinterstore-empty-1"; + const key2 = "zinterstore-empty-2"; + const dest = "zinterstore-empty-dest"; + + // Set up sorted sets with no common members + await redis.zadd(key1, "1", "a", "2", "b"); + await redis.zadd(key2, "1", "c", "2", "d"); + + // Empty intersection + const count = await redis.zinterstore(dest, 2, key1, key2); + expect(count).toBe(0); + + // Verify destination is empty + const exists = await redis.exists(dest); + expect(exists).toBe(false); + }); + + test("should compute union with ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-test-1"; + const key2 = "zunion-test-2"; + + // Set up sorted sets + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "4", "b", "5", "c", "6", "d"); + + // Basic union - returns all members from both sets + const result1 = await redis.zunion(2, key1, key2); + expect(result1).toEqual(["a", "b", "d", "c"]); + + // With WITHSCORES (scores are summed by default) + const result2 = await redis.zunion(2, key1, key2, "WITHSCORES"); + expect(result2).toEqual([ + ["a", 1], + ["b", 6], + ["d", 6], + ["c", 8], + ]); + }); + + test("should compute union with WEIGHTS in ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-weights-1"; + const key2 = "zunion-weights-2"; + + // Set up sorted sets + await redis.zadd(key1, "1", "x", "2", "y", "3", "z"); + await redis.zadd(key2, "2", "y", "3", "z", "4", "w"); + + // With weights - multiply scores before aggregation + const result = await redis.zunion(2, key1, key2, "WEIGHTS", "2", "3", "WITHSCORES"); + expect(result).toEqual([ + ["x", 2], + ["y", 10], + ["w", 12], + ["z", 15], + ]); + }); + + test("should compute union with AGGREGATE MIN in ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-min-1"; + const key2 = "zunion-min-2"; + + // Set up sorted sets + await redis.zadd(key1, "1", "p", "3", "q"); + await redis.zadd(key2, "2", "p", "1", "q"); + + // With MIN aggregation - take minimum score + const result = await redis.zunion(2, key1, key2, "AGGREGATE", "MIN", "WITHSCORES"); + expect(result).toEqual([ + ["p", 1], + ["q", 1], + ]); + }); + + test("should compute union with AGGREGATE MAX in ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-max-1"; + const key2 = "zunion-max-2"; + + // Set up sorted sets + await redis.zadd(key1, "1", "r", "3", "s"); + await redis.zadd(key2, "2", "r", "1", "s"); + + // With MAX aggregation - take maximum score + const result = await redis.zunion(2, key1, key2, "AGGREGATE", "MAX", "WITHSCORES"); + expect(result).toEqual([ + ["r", 2], + ["s", 3], + ]); + }); + + test("should compute union with single set in ZUNION", async () => { + const redis = ctx.redis; + const key = "zunion-single"; + + // Set up single sorted set + await redis.zadd(key, "1", "one", "2", "two", "3", "three"); + + // Union of single set + const result = await redis.zunion(1, key); + expect(result).toEqual(["one", "two", "three"]); + }); + + test("should compute union with three sets in ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-three-1"; + const key2 = "zunion-three-2"; + const key3 = "zunion-three-3"; + + // Set up three sorted sets + await redis.zadd(key1, "1", "a", "2", "b"); + await redis.zadd(key2, "2", "b", "3", "c"); + await redis.zadd(key3, "3", "c", "4", "d"); + + // Union of three sets + const result = await redis.zunion(3, key1, key2, key3, "WITHSCORES"); + expect(result).toEqual([ + ["a", 1], + ["b", 4], + ["d", 4], + ["c", 6], + ]); + }); + + test("should handle empty set in ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-empty-1"; + const key2 = "zunion-empty-2"; + + // Set up one sorted set, leave other empty + await redis.zadd(key1, "1", "a", "2", "b"); + + // Union with empty set + const result = await redis.zunion(2, key1, key2); + expect(result).toEqual(["a", "b"]); + }); + + test("should store union with ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-test-1"; + const key2 = "zunionstore-test-2"; + const dest = "zunionstore-dest"; + + // Set up sorted sets + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "4", "b", "5", "c", "6", "d"); + + // Basic union store + const count = await redis.zunionstore(dest, 2, key1, key2); + expect(count).toBe(4); + + // Verify stored members + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["a", 1], + ["b", 6], + ["d", 6], + ["c", 8], + ]); + }); + + test("should store union with WEIGHTS in ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-weights-1"; + const key2 = "zunionstore-weights-2"; + const dest = "zunionstore-weights-dest"; + + // Set up sorted sets + await redis.zadd(key1, "1", "x", "2", "y"); + await redis.zadd(key2, "2", "x", "3", "y"); + + // With weights + const count = await redis.zunionstore(dest, 2, key1, key2, "WEIGHTS", "2", "3"); + expect(count).toBe(2); + + // Verify stored members with weighted scores + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["x", 8], + ["y", 13], + ]); + }); + + test("should store union with AGGREGATE MIN in ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-agg-min-1"; + const key2 = "zunionstore-agg-min-2"; + const dest = "zunionstore-agg-min-dest"; + + // Set up sorted sets + await redis.zadd(key1, "1", "m", "3", "n"); + await redis.zadd(key2, "2", "m", "1", "n"); + + // With MIN aggregation + const count = await redis.zunionstore(dest, 2, key1, key2, "AGGREGATE", "MIN"); + expect(count).toBe(2); + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["m", 1], + ["n", 1], + ]); + }); + + test("should store union with AGGREGATE MAX in ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-agg-max-1"; + const key2 = "zunionstore-agg-max-2"; + const dest = "zunionstore-agg-max-dest"; + + // Set up sorted sets + await redis.zadd(key1, "1", "m", "3", "n"); + await redis.zadd(key2, "2", "m", "1", "n"); + + // With MAX aggregation + const count = await redis.zunionstore(dest, 2, key1, key2, "AGGREGATE", "MAX"); + expect(count).toBe(2); + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["m", 2], + ["n", 3], + ]); + }); + + test("should overwrite existing destination with ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-overwrite-1"; + const key2 = "zunionstore-overwrite-2"; + const dest = "zunionstore-overwrite-dest"; + + // Set up initial destination + await redis.zadd(dest, "100", "old"); + + // Set up sorted sets + await redis.zadd(key1, "1", "a", "2", "b"); + await redis.zadd(key2, "3", "c"); + + // Union store should overwrite + const count = await redis.zunionstore(dest, 2, key1, key2); + expect(count).toBe(3); + + // Verify old member is gone + const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); + expect(stored).toEqual(["a", "b", "c"]); + expect(stored).not.toContain("old"); + }); + + test("should handle empty sets with ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-empty-1"; + const key2 = "zunionstore-empty-2"; + const dest = "zunionstore-empty-dest"; + + // Both sets empty + const count = await redis.zunionstore(dest, 2, key1, key2); + expect(count).toBe(0); + + // Verify destination is empty + const exists = await redis.exists(dest); + expect(exists).toBe(false); + }); + + test("should reject invalid numkeys in ZUNION", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zunion(-1, "key1"); + }).toThrowErrorMatchingInlineSnapshot(`"ERR at least 1 input key is needed for 'zunion' command"`); + }); + + test("should reject invalid key in ZUNION", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zunion(1, {} as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zunion'."`); + }); + + test("should reject invalid destination in ZUNIONSTORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zunionstore([] as any, 2, "key1", "key2"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zunionstore'."`, + ); + }); + + test("should reject invalid source key in ZUNIONSTORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zunionstore("dest", 2, "key1", null as any); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zunionstore'."`, + ); + }); }); describe("Connection State", () => { From 8ca3c8cfce7d4454f1a9d348f0cfedd8a56289b2 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 22:11:00 -0700 Subject: [PATCH 11/45] impl blocking zmpop/bzmpop/bzmpopmin/bzmpopmax --- packages/bun-types/redis.d.ts | 77 +++++++++ src/bun.js/api/valkey.classes.ts | 16 ++ src/valkey/js_valkey.zig | 4 + src/valkey/js_valkey_functions.zig | 4 + test/js/valkey/valkey.test.ts | 261 +++++++++++++++++++++++++++++ 5 files changed, 362 insertions(+) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 00b366deb3e7..660d5fbc6e6b 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -689,6 +689,38 @@ declare module "bun" { */ zpopmin(key: RedisClient.KeyLike): Promise; + /** + * Remove and return the member with the lowest score from one or more sorted sets, or block until one is available + * @param args Keys followed by timeout in seconds (e.g., "key1", "key2", 1.0) + * @returns Promise that resolves with [key, member, score] or null if timeout + * @example + * ```ts + * // Block for up to 1 second waiting for an element + * const result = await redis.bzpopmin("myzset", 1.0); + * if (result) { + * const [key, member, score] = result; + * console.log(`Popped ${member} with score ${score} from ${key}`); + * } + * ``` + */ + bzpopmin(...args: (RedisClient.KeyLike | number)[]): Promise<[string, string, number] | null>; + + /** + * Remove and return the member with the highest score from one or more sorted sets, or block until one is available + * @param args Keys followed by timeout in seconds (e.g., "key1", "key2", 1.0) + * @returns Promise that resolves with [key, member, score] or null if timeout + * @example + * ```ts + * // Block for up to 1 second waiting for an element + * const result = await redis.bzpopmax("myzset", 1.0); + * if (result) { + * const [key, member, score] = result; + * console.log(`Popped ${member} with score ${score} from ${key}`); + * } + * ``` + */ + bzpopmax(...args: (RedisClient.KeyLike | number)[]): Promise<[string, string, number] | null>; + /** * Get one or multiple random members from a sorted set * @param key The sorted set key @@ -2267,6 +2299,51 @@ declare module "bun" { * ``` */ zunionstore(destination: RedisClient.KeyLike, numkeys: number, ...args: (string | number)[]): Promise; + + /** + * Remove and return members with scores from one or more sorted sets. + * Pops from the first non-empty sorted set. + * + * @example + * ```ts + * // Pop lowest score from one set + * const result1 = await redis.zmpop(1, "myzset", "MIN"); + * // Returns: ["myzset", [["member1", 1]]] + * + * // Pop highest score from multiple sets + * const result2 = await redis.zmpop(2, "zset1", "zset2", "MAX"); + * // Returns: ["zset1", [["member5", 5]]] (pops from first non-empty) + * + * // Pop multiple members + * const result3 = await redis.zmpop(1, "myzset", "MIN", "COUNT", 3); + * // Returns: ["myzset", [["member1", 1], ["member2", 2], ["member3", 3]]] + * + * // Empty set returns null + * const result4 = await redis.zmpop(1, "emptyset", "MIN"); + * // Returns: null + * ``` + */ + zmpop(numkeys: number, ...args: (string | number)[]): Promise<[string, [string, number][]] | null>; + + /** + * Blocking version of ZMPOP. Blocks until a member is available or timeout expires. + * + * @example + * ```ts + * // Block for 5 seconds waiting for a member + * const result1 = await redis.bzmpop(5, 1, "myzset", "MIN"); + * // Returns: ["myzset", [["member1", 1]]] or null if timeout + * + * // Block indefinitely (timeout 0) + * const result2 = await redis.bzmpop(0, 2, "zset1", "zset2", "MAX"); + * // Returns: ["zset1", [["member5", 5]]] + * + * // Block with COUNT option + * const result3 = await redis.bzmpop(1, 1, "myzset", "MIN", "COUNT", 2); + * // Returns: ["myzset", [["member1", 1], ["member2", 2]]] or null if timeout + * ``` + */ + bzmpop(timeout: number, numkeys: number, ...args: (string | number)[]): Promise<[string, [string, number][]] | null>; } /** diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 2e9bd9033f87..e2da425471fb 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -340,6 +340,22 @@ export default [ fn: "zunionstore", length: 3, }, + zmpop: { + fn: "zmpop", + length: 2, + }, + bzmpop: { + fn: "bzmpop", + length: 3, + }, + bzpopmin: { + fn: "bzpopmin", + length: 2, + }, + bzpopmax: { + fn: "bzpopmax", + length: 2, + }, mget: { fn: "mget", }, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 45ba5ca2d704..6188c7c0211f 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1354,6 +1354,10 @@ pub const JSValkeyClient = struct { pub const zinterstore = fns.zinterstore; pub const zunion = fns.zunion; pub const zunionstore = fns.zunionstore; + pub const zmpop = fns.zmpop; + pub const bzmpop = fns.bzmpop; + pub const bzpopmin = fns.bzpopmin; + pub const bzpopmax = fns.bzpopmax; const fns = @import("./js_valkey_functions.zig"); }; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index ccbfac54602b..5c90458d7314 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -697,6 +697,10 @@ pub const zintercard = compile.@"(...strings: string[])"("zintercard", "ZINTERCA pub const zinterstore = compile.@"(...strings: string[])"("zinterstore", "ZINTERSTORE", .not_subscriber).call; pub const zunion = compile.@"(...strings: string[])"("zunion", "ZUNION", .not_subscriber).call; pub const zunionstore = compile.@"(...strings: string[])"("zunionstore", "ZUNIONSTORE", .not_subscriber).call; +pub const zmpop = compile.@"(...strings: string[])"("zmpop", "ZMPOP", .not_subscriber).call; +pub const bzmpop = compile.@"(...strings: string[])"("bzmpop", "BZMPOP", .not_subscriber).call; +pub const bzpopmin = compile.@"(...strings: string[])"("bzpopmin", "BZPOPMIN", .not_subscriber).call; +pub const bzpopmax = compile.@"(...strings: string[])"("bzpopmax", "BZPOPMAX", .not_subscriber).call; pub const del = compile.@"(key: RedisKey, ...args: RedisKey[])"("del", "DEL", "key", .not_subscriber).call; pub const mget = compile.@"(key: RedisKey, ...args: RedisKey[])"("mget", "MGET", "key", .not_subscriber).call; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index fbe3858c8c55..07b43fae1109 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -2473,6 +2473,267 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { `"Expected additional arguments to be a string or buffer for 'zunionstore'."`, ); }); + + test("should pop members with MIN option using ZMPOP", async () => { + const redis = ctx.redis; + const key = "zmpop-min-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Pop member with lowest score + const result1 = await redis.zmpop(1, key, "MIN"); + expect(result1).toBeDefined(); + expect(result1).not.toBeNull(); + expect(result1![0]).toBe(key); + expect(result1![1]).toEqual([["one", 1]]); + + // Verify remaining count + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(4); + }); + + test("should pop members with MAX option using ZMPOP", async () => { + const redis = ctx.redis; + const key = "zmpop-max-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Pop member with highest score + const result1 = await redis.zmpop(1, key, "MAX"); + expect(result1).toBeDefined(); + expect(result1).not.toBeNull(); + expect(result1![0]).toBe(key); + expect(result1![1]).toEqual([["five", 5]]); + + // Verify remaining count + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(4); + }); + + test("should pop multiple members with COUNT option using ZMPOP", async () => { + const redis = ctx.redis; + const key = "zmpop-count-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Pop 3 members with lowest scores + const result = await redis.zmpop(1, key, "MIN", "COUNT", 3); + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(result![0]).toBe(key); + expect(result![1]).toEqual([ + ["one", 1], + ["two", 2], + ["three", 3], + ]); + + // Verify remaining count + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should return null when ZMPOP on empty set", async () => { + const redis = ctx.redis; + const emptyKey = "zmpop-empty-test"; + + // Try to pop from empty set + const result = await redis.zmpop(1, emptyKey, "MIN"); + expect(result).toBeNull(); + }); + + test("should pop from first non-empty set with ZMPOP", async () => { + const redis = ctx.redis; + const key1 = "zmpop-multi-test1"; + const key2 = "zmpop-multi-test2"; + const key3 = "zmpop-multi-test3"; + + // Only populate key2 + await redis.send("ZADD", [key2, "1", "one", "2", "two"]); + + // Pop from multiple keys (should get from key2) + const result = await redis.zmpop(3, key1, key2, key3, "MIN"); + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(result![0]).toBe(key2); + expect(result![1]).toEqual([["one", 1]]); + }); + + test("should block and pop with BZMPOP", async () => { + const redis = ctx.redis; + const key = "bzmpop-test"; + + // Add a member to the set + await redis.send("ZADD", [key, "1", "one", "2", "two"]); + + // Use short timeout for testing + const result = await redis.bzmpop(0.1, 1, key, "MIN"); + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(result![0]).toBe(key); + expect(result![1]).toEqual([["one", 1]]); + + // Verify member was removed + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(1); + }); + + test("should timeout with BZMPOP on empty set", async () => { + const redis = ctx.redis; + const emptyKey = "bzmpop-timeout-test"; + + // Try to pop from empty set with short timeout + const result = await redis.bzmpop(0.1, 1, emptyKey, "MIN"); + expect(result).toBeNull(); + }); + + test("should block and pop multiple members with BZMPOP COUNT", async () => { + const redis = ctx.redis; + const key = "bzmpop-count-test"; + + // Add members to the set + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); + + // Pop 2 members with short timeout + const result = await redis.bzmpop(0.5, 1, key, "MAX", "COUNT", 2); + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(result![0]).toBe(key); + expect(result![1]).toEqual([ + ["three", 3], + ["two", 2], + ]); + + // Verify one member remains + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(1); + }); + + test("should reject invalid arguments in ZMPOP", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zmpop({} as any, "key1", "MIN"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zmpop'."`); + }); + + test("should reject invalid arguments in BZMPOP", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.bzmpop(1, {} as any, "key1", "MIN"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'bzmpop'."`, + ); + }); + + test("should block and pop lowest score with BZPOPMIN", async () => { + const redis = ctx.redis; + const key = "bzpopmin-test"; + + // Add members to sorted set + await redis.send("ZADD", [key, "1.0", "one", "2.0", "two", "3.0", "three"]); + + // Pop lowest score with short timeout + const result = await redis.bzpopmin(key, 0.1); + expect(result).toBeDefined(); + expect(result).toHaveLength(3); + expect(result![0]).toBe(key); + expect(result![1]).toBe("one"); + expect(result![2]).toBe(1); + + // Verify member was removed + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should timeout with BZPOPMIN when no elements available", async () => { + const redis = ctx.redis; + const key = "bzpopmin-empty-test"; + + // Try to pop from non-existent key with short timeout + const result = await redis.bzpopmin(key, 0.1); + expect(result).toBeNull(); + }); + + test("should block and pop highest score with BZPOPMAX", async () => { + const redis = ctx.redis; + const key = "bzpopmax-test"; + + // Add members to sorted set + await redis.send("ZADD", [key, "1.0", "one", "2.0", "two", "3.0", "three"]); + + // Pop highest score with short timeout + const result = await redis.bzpopmax(key, 0.1); + expect(result).toBeDefined(); + expect(result).toHaveLength(3); + expect(result![0]).toBe(key); + expect(result![1]).toBe("three"); + expect(result![2]).toBe(3); + + // Verify member was removed + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should timeout with BZPOPMAX when no elements available", async () => { + const redis = ctx.redis; + const key = "bzpopmax-empty-test"; + + // Try to pop from non-existent key with short timeout + const result = await redis.bzpopmax(key, 0.1); + expect(result).toBeNull(); + }); + + test("should work with multiple keys in BZPOPMIN", async () => { + const redis = ctx.redis; + const key1 = "bzpopmin-multi-1"; + const key2 = "bzpopmin-multi-2"; + + // Add members to second key only + await redis.send("ZADD", [key2, "5.0", "five", "6.0", "six"]); + + // Pop from multiple keys (should return from key2) + const result = await redis.bzpopmin(key1, key2, 0.1); + expect(result).toBeDefined(); + expect(result![0]).toBe(key2); + expect(result![1]).toBe("five"); + expect(result![2]).toBe(5); + }); + + test("should work with multiple keys in BZPOPMAX", async () => { + const redis = ctx.redis; + const key1 = "bzpopmax-multi-1"; + const key2 = "bzpopmax-multi-2"; + + // Add members to second key only + await redis.send("ZADD", [key2, "5.0", "five", "6.0", "six"]); + + // Pop from multiple keys (should return from key2) + const result = await redis.bzpopmax(key1, key2, 0.5); + expect(result).toBeDefined(); + expect(result![0]).toBe(key2); + expect(result![1]).toBe("six"); + expect(result![2]).toBe(6); + }); + + test("should reject invalid arguments in BZPOPMIN", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.bzpopmin({} as any, 1); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'bzpopmin'."`, + ); + }); + + test("should reject invalid arguments in BZPOPMAX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.bzpopmax([] as any, 1); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'bzpopmax'."`, + ); + }); }); describe("Connection State", () => { From f7ea775633c7cea41e88a0fcfd272edce15ea523 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 22:23:24 -0700 Subject: [PATCH 12/45] feat: Implement Redis list operations --- packages/bun-types/redis.d.ts | 367 +++++++++++- src/bun.js/api/valkey.classes.ts | 60 ++ src/valkey/js_valkey.zig | 15 + src/valkey/js_valkey_functions.zig | 15 + test/js/valkey/valkey.test.ts | 886 +++++++++++++++++++++++++++++ 5 files changed, 1339 insertions(+), 4 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 660d5fbc6e6b..c6ddc5675e9d 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -436,6 +436,157 @@ declare module "bun" { */ keys(pattern: string): Promise; + /** + * Blocking pop from head of one or more lists + * + * Blocks until an element is available in one of the lists or the timeout expires. + * Checks keys in order and pops from the first non-empty list. + * + * @param args Keys followed by timeout in seconds (can be fractional, 0 = block indefinitely) + * @returns Promise that resolves with [key, element] or null on timeout + * + * @example + * ```ts + * // Block for up to 1 second + * const result = await redis.blpop("mylist", 1.0); + * if (result) { + * const [key, element] = result; + * console.log(`Popped ${element} from ${key}`); + * } + * + * // Block indefinitely (timeout = 0) + * const result2 = await redis.blpop("list1", "list2", 0); + * ``` + */ + blpop(...args: (RedisClient.KeyLike | number)[]): Promise<[string, string] | null>; + + /** + * Blocking pop from tail of one or more lists + * + * Blocks until an element is available in one of the lists or the timeout expires. + * Checks keys in order and pops from the first non-empty list. + * + * @param args Keys followed by timeout in seconds (can be fractional, 0 = block indefinitely) + * @returns Promise that resolves with [key, element] or null on timeout + * + * @example + * ```ts + * // Block for up to 1 second + * const result = await redis.brpop("mylist", 1.0); + * if (result) { + * const [key, element] = result; + * console.log(`Popped ${element} from ${key}`); + * } + * + * // Block indefinitely (timeout = 0) + * const result2 = await redis.brpop("list1", "list2", 0); + * ``` + */ + brpop(...args: (RedisClient.KeyLike | number)[]): Promise<[string, string] | null>; + + /** + * Blocking move from one list to another + * + * Atomically moves an element from source to destination list, blocking until an element is available + * or the timeout expires. Allows specifying which end to pop from (LEFT/RIGHT) and which end to push to (LEFT/RIGHT). + * + * @param source Source list key + * @param destination Destination list key + * @param from Direction to pop from source: "LEFT" or "RIGHT" + * @param to Direction to push to destination: "LEFT" or "RIGHT" + * @param timeout Timeout in seconds (can be fractional, 0 = block indefinitely) + * @returns Promise that resolves with the moved element or null on timeout + * + * @example + * ```ts + * // Move from right of source to left of destination (like BRPOPLPUSH) + * const element = await redis.blmove("mylist", "otherlist", "RIGHT", "LEFT", 1.0); + * if (element) { + * console.log(`Moved element: ${element}`); + * } + * + * // Move from left to left + * await redis.blmove("list1", "list2", "LEFT", "LEFT", 0.5); + * ``` + */ + blmove( + source: RedisClient.KeyLike, + destination: RedisClient.KeyLike, + from: "LEFT" | "RIGHT", + to: "LEFT" | "RIGHT", + timeout: number, + ): Promise; + + /** + * Blocking pop multiple elements from lists + * + * Blocks until an element is available from one of the specified lists or the timeout expires. + * Can pop from the LEFT or RIGHT end and optionally pop multiple elements at once using COUNT. + * + * @param timeout Timeout in seconds (can be fractional, 0 = block indefinitely) + * @param numkeys Number of keys that follow + * @param args Keys, direction ("LEFT" or "RIGHT"), and optional COUNT modifier + * @returns Promise that resolves with [key, [elements]] or null on timeout + * + * @example + * ```ts + * // Pop from left end of first available list, wait 1 second + * const result = await redis.blmpop(1.0, 2, "list1", "list2", "LEFT"); + * if (result) { + * const [key, elements] = result; + * console.log(`Popped from ${key}: ${elements.join(", ")}`); + * } + * + * // Pop 3 elements from right end + * const result2 = await redis.blmpop(0.5, 1, "mylist", "RIGHT", "COUNT", 3); + * // Returns: ["mylist", ["elem1", "elem2", "elem3"]] or null if timeout + * ``` + */ + blmpop(timeout: number, numkeys: number, ...args: (string | number)[]): Promise<[string, string[]] | null>; + + /** + * Blocking right pop from source and left push to destination + * + * Atomically pops an element from the tail of source list and pushes it to the head of destination list, + * blocking until an element is available or the timeout expires. This is the blocking version of RPOPLPUSH. + * + * @param source Source list key + * @param destination Destination list key + * @param timeout Timeout in seconds (can be fractional, 0 = block indefinitely) + * @returns Promise that resolves with the moved element or null on timeout + * + * @example + * ```ts + * // Block for up to 1 second + * const element = await redis.brpoplpush("tasks", "processing", 1.0); + * if (element) { + * console.log(`Processing task: ${element}`); + * } else { + * console.log("No tasks available"); + * } + * + * // Block indefinitely (timeout = 0) + * const task = await redis.brpoplpush("queue", "active", 0); + * ``` + */ + brpoplpush(source: RedisClient.KeyLike, destination: RedisClient.KeyLike, timeout: number): Promise; + + /** + * Get element at index from a list + * @param key The list key + * @param index Zero-based index (negative indexes count from the end, -1 is last element) + * @returns Promise that resolves with the element at index, or null if index is out of range + * + * @example + * ```ts + * await redis.lpush("mylist", "three", "two", "one"); + * console.log(await redis.lindex("mylist", 0)); // "one" + * console.log(await redis.lindex("mylist", -1)); // "three" + * console.log(await redis.lindex("mylist", 5)); // null + * ``` + */ + lindex(key: RedisClient.KeyLike, index: number): Promise; + /** * Get the length of a list * @param key The list key @@ -443,14 +594,137 @@ declare module "bun" { */ llen(key: RedisClient.KeyLike): Promise; + /** + * Atomically pop an element from a source list and push it to a destination list + * + * Pops an element from the source list (from LEFT or RIGHT) and pushes it + * to the destination list (to LEFT or RIGHT). + * + * @param source The source list key + * @param destination The destination list key + * @param from Direction to pop from source: "LEFT" (head) or "RIGHT" (tail) + * @param to Direction to push to destination: "LEFT" (head) or "RIGHT" (tail) + * @returns Promise that resolves with the element moved, or null if the source list is empty + * + * @example + * ```ts + * await redis.lpush("source", "a", "b", "c"); + * const result1 = await redis.lmove("source", "dest", "LEFT", "RIGHT"); + * // result1: "c" (popped from head of source, pushed to tail of dest) + * + * const result2 = await redis.lmove("source", "dest", "RIGHT", "LEFT"); + * // result2: "a" (popped from tail of source, pushed to head of dest) + * ``` + */ + lmove( + source: RedisClient.KeyLike, + destination: RedisClient.KeyLike, + from: "LEFT" | "RIGHT", + to: "LEFT" | "RIGHT", + ): Promise; + /** * Remove and get the first element in a list * @param key The list key - * @returns Promise that resolves with the first element, or null if the - * list is empty + * @returns Promise that resolves with the first element, or null if the list is empty */ lpop(key: RedisClient.KeyLike): Promise; + /** + * Find the position(s) of an element in a list + * + * Returns the index of matching elements inside a Redis list. + * By default, returns the index of the first match. Use RANK to find the nth occurrence, + * COUNT to get multiple positions, and MAXLEN to limit the search. + * + * @param key The list key + * @param element The element to search for + * @param options Optional arguments: "RANK", rank, "COUNT", num, "MAXLEN", len + * @returns Promise that resolves with the index (number), an array of indices (number[]), + * or null if element is not found. Returns array when COUNT option is used. + * + * @example + * ```ts + * await redis.lpush("mylist", "a", "b", "c", "b", "d"); + * const pos1 = await redis.lpos("mylist", "b"); + * // pos1: 1 (first occurrence of "b") + * + * const pos2 = await redis.lpos("mylist", "b", "RANK", 2); + * // pos2: 3 (second occurrence of "b") + * + * const positions = await redis.lpos("mylist", "b", "COUNT", 0); + * // positions: [1, 3] (all occurrences of "b") + * + * const pos3 = await redis.lpos("mylist", "x"); + * // pos3: null (element not found) + * ``` + */ + lpos( + key: RedisClient.KeyLike, + element: RedisClient.ValueLike, + ...options: (string | number)[] + ): Promise; + + /** + * Pop one or more elements from one or more lists + * + * Pops elements from the first non-empty list in the specified order (LEFT = from head, RIGHT = from tail). + * Optionally specify COUNT to pop multiple elements at once. + * + * @param numkeys The number of keys that follow + * @param args Keys followed by LEFT or RIGHT, optionally followed by "COUNT" and count value + * @returns Promise that resolves with [key, [elements]] or null if all lists are empty + * + * @example + * ```ts + * await redis.lpush("list1", "a", "b", "c"); + * const result1 = await redis.lmpop(1, "list1", "LEFT"); + * // result1: ["list1", ["c"]] + * + * const result2 = await redis.lmpop(1, "list1", "RIGHT", "COUNT", 2); + * // result2: ["list1", ["a", "b"]] + * + * const result3 = await redis.lmpop(2, "emptylist", "list1", "LEFT"); + * // result3: null (if both lists are empty) + * ``` + */ + lmpop(numkeys: number, ...args: (string | number)[]): Promise<[string, string[]] | null>; + + /** + * Get a range of elements from a list + * @param key The list key + * @param start Zero-based start index (negative indexes count from the end) + * @param stop Zero-based stop index (negative indexes count from the end) + * @returns Promise that resolves with array of elements in the specified range + * + * @example + * ```ts + * await redis.lpush("mylist", "three", "two", "one"); + * console.log(await redis.lrange("mylist", 0, -1)); // ["one", "two", "three"] + * console.log(await redis.lrange("mylist", 0, 1)); // ["one", "two"] + * console.log(await redis.lrange("mylist", -2, -1)); // ["two", "three"] + * ``` + */ + lrange(key: RedisClient.KeyLike, start: number, stop: number): Promise; + + /** + * Set element at index in a list + * @param key The list key + * @param index Zero-based index (negative indexes count from the end) + * @param element The value to set + * @returns Promise that resolves with "OK" on success + * + * @example + * ```ts + * await redis.lpush("mylist", "three", "two", "one"); + * await redis.lset("mylist", 0, "zero"); + * console.log(await redis.lrange("mylist", 0, -1)); // ["zero", "two", "three"] + * await redis.lset("mylist", -1, "last"); + * console.log(await redis.lrange("mylist", 0, -1)); // ["zero", "two", "last"] + * ``` + */ + lset(key: RedisClient.KeyLike, index: number, element: RedisClient.KeyLike): Promise; + /** * Remove the expiration from a key * @param key The key to persist @@ -509,6 +783,29 @@ declare module "bun" { */ rpop(key: RedisClient.KeyLike): Promise; + /** + * Atomically pop the last element from a source list and push it to the head of a destination list + * + * This is equivalent to LMOVE with "RIGHT" "LEFT". It's an atomic operation that removes + * the last element (tail) from the source list and pushes it to the head of the destination list. + * + * @param source The source list key + * @param destination The destination list key + * @returns Promise that resolves with the element moved, or null if the source list is empty + * + * @example + * ```ts + * await redis.lpush("source", "a", "b", "c"); + * // source: ["c", "b", "a"] + * + * const result = await redis.rpoplpush("source", "dest"); + * // result: "a" (removed from tail of source, added to head of dest) + * // source: ["c", "b"] + * // dest: ["a"] + * ``` + */ + rpoplpush(source: RedisClient.KeyLike, destination: RedisClient.KeyLike): Promise; + /** * Incrementally iterate the keyspace * @@ -912,6 +1209,29 @@ declare module "bun" { */ getset(key: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + /** + * Insert an element before or after another element in a list + * @param key The list key + * @param position "BEFORE" or "AFTER" to specify where to insert + * @param pivot The pivot element to insert before or after + * @param element The element to insert + * @returns Promise that resolves with the length of the list after insert, -1 if pivot not found, or 0 if key doesn't exist + * + * @example + * ```ts + * await redis.lpush("mylist", "World"); + * await redis.lpush("mylist", "Hello"); + * await redis.linsert("mylist", "BEFORE", "World", "There"); + * // List is now: ["Hello", "There", "World"] + * ``` + */ + linsert( + key: RedisClient.KeyLike, + position: "BEFORE" | "AFTER", + pivot: RedisClient.KeyLike, + element: RedisClient.KeyLike, + ): Promise; + /** * Prepend one or multiple values to a list * @param key The list key @@ -919,7 +1239,7 @@ declare module "bun" { * @returns Promise that resolves with the length of the list after the push * operation */ - lpush(key: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + lpush(key: RedisClient.KeyLike, value: RedisClient.KeyLike, ...rest: RedisClient.KeyLike[]): Promise; /** * Prepend a value to a list, only if the list exists @@ -930,6 +1250,41 @@ declare module "bun" { */ lpushx(key: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + /** + * Remove elements from a list + * @param key The list key + * @param count Number of elements to remove + * - count > 0: Remove count occurrences from head to tail + * - count < 0: Remove count occurrences from tail to head + * - count = 0: Remove all occurrences + * @param element The element to remove + * @returns Promise that resolves with the number of elements removed + * + * @example + * ```ts + * await redis.rpush("mylist", "hello", "hello", "world", "hello"); + * await redis.lrem("mylist", 2, "hello"); // Removes first 2 "hello" + * // List is now: ["world", "hello"] + * ``` + */ + lrem(key: RedisClient.KeyLike, count: number, element: RedisClient.KeyLike): Promise; + + /** + * Trim a list to the specified range + * @param key The list key + * @param start The start index (0-based, can be negative) + * @param stop The stop index (0-based, can be negative) + * @returns Promise that resolves with "OK" + * + * @example + * ```ts + * await redis.rpush("mylist", "one", "two", "three", "four"); + * await redis.ltrim("mylist", 1, 2); + * // List is now: ["two", "three"] + * ``` + */ + ltrim(key: RedisClient.KeyLike, start: number, stop: number): Promise; + /** * Add one or more members to a HyperLogLog * @param key The HyperLogLog key @@ -2343,7 +2698,11 @@ declare module "bun" { * // Returns: ["myzset", [["member1", 1], ["member2", 2]]] or null if timeout * ``` */ - bzmpop(timeout: number, numkeys: number, ...args: (string | number)[]): Promise<[string, [string, number][]] | null>; + bzmpop( + timeout: number, + numkeys: number, + ...args: (string | number)[] + ): Promise<[string, [string, number][]] | null>; } /** diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index e2da425471fb..f4d59c94b5b0 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -143,6 +143,26 @@ export default [ bitcount: { fn: "bitcount", }, + blmove: { + fn: "blmove", + length: 5, + }, + blmpop: { + fn: "blmpop", + length: 3, + }, + blpop: { + fn: "blpop", + length: 2, + }, + brpop: { + fn: "brpop", + length: 2, + }, + brpoplpush: { + fn: "brpoplpush", + length: 3, + }, getbit: { fn: "getbit", }, @@ -182,12 +202,48 @@ export default [ keys: { fn: "keys", }, + lindex: { + fn: "lindex", + length: 2, + }, + linsert: { + fn: "linsert", + length: 4, + }, llen: { fn: "llen", }, + lmove: { + fn: "lmove", + length: 4, + }, + lmpop: { + fn: "lmpop", + length: 2, + }, lpop: { fn: "lpop", }, + lpos: { + fn: "lpos", + length: 2, + }, + lrange: { + fn: "lrange", + length: 3, + }, + lrem: { + fn: "lrem", + length: 3, + }, + lset: { + fn: "lset", + length: 3, + }, + ltrim: { + fn: "ltrim", + length: 3, + }, persist: { fn: "persist", }, @@ -207,6 +263,10 @@ export default [ rpop: { fn: "rpop", }, + rpoplpush: { + fn: "rpoplpush", + length: 2, + }, scan: { fn: "scan", }, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 6188c7c0211f..eb9d69c98fc0 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1243,6 +1243,8 @@ pub const JSValkeyClient = struct { pub const @"type" = fns.type; pub const append = fns.append; pub const bitcount = fns.bitcount; + pub const blpop = fns.blpop; + pub const brpop = fns.brpop; pub const copy = fns.copy; pub const decr = fns.decr; pub const decrby = fns.decrby; @@ -1274,10 +1276,19 @@ pub const JSValkeyClient = struct { pub const incrby = fns.incrby; pub const incrbyfloat = fns.incrbyfloat; pub const keys = fns.keys; + pub const lindex = fns.lindex; + pub const linsert = fns.linsert; pub const llen = fns.llen; + pub const lmove = fns.lmove; + pub const lmpop = fns.lmpop; pub const lpop = fns.lpop; + pub const lpos = fns.lpos; pub const lpush = fns.lpush; pub const lpushx = fns.lpushx; + pub const lrange = fns.lrange; + pub const lrem = fns.lrem; + pub const lset = fns.lset; + pub const ltrim = fns.ltrim; pub const mget = fns.mget; pub const mset = fns.mset; pub const msetnx = fns.msetnx; @@ -1297,6 +1308,7 @@ pub const JSValkeyClient = struct { pub const rename = fns.rename; pub const renamenx = fns.renamenx; pub const rpop = fns.rpop; + pub const rpoplpush = fns.rpoplpush; pub const rpush = fns.rpush; pub const rpushx = fns.rpushx; pub const sadd = fns.sadd; @@ -1358,6 +1370,9 @@ pub const JSValkeyClient = struct { pub const bzmpop = fns.bzmpop; pub const bzpopmin = fns.bzpopmin; pub const bzpopmax = fns.bzpopmax; + pub const blmove = fns.blmove; + pub const blmpop = fns.blmpop; + pub const brpoplpush = fns.brpoplpush; const fns = @import("./js_valkey_functions.zig"); }; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 5c90458d7314..60ba76c8f2a6 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -631,6 +631,11 @@ pub fn ping(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: } pub const bitcount = compile.@"(key: RedisKey)"("bitcount", "BITCOUNT", "key", .not_subscriber).call; +pub const blmove = compile.@"(...strings: string[])"("blmove", "BLMOVE", .not_subscriber).call; +pub const blmpop = compile.@"(...strings: string[])"("blmpop", "BLMPOP", .not_subscriber).call; +pub const blpop = compile.@"(...strings: string[])"("blpop", "BLPOP", .not_subscriber).call; +pub const brpop = compile.@"(...strings: string[])"("brpop", "BRPOP", .not_subscriber).call; +pub const brpoplpush = compile.@"(...strings: string[])"("brpoplpush", "BRPOPLPUSH", .not_subscriber).call; pub const getbit = compile.@"(key: RedisKey, value: RedisValue)"("getbit", "GETBIT", "key", "offset", .not_subscriber).call; pub const setbit = compile.@"(...strings: string[])"("setbit", "SETBIT", .not_subscriber).call; pub const getrange = compile.@"(...strings: string[])"("getrange", "GETRANGE", .not_subscriber).call; @@ -645,8 +650,17 @@ pub const hkeys = compile.@"(key: RedisKey)"("hkeys", "HKEYS", "key", .not_subsc pub const hlen = compile.@"(key: RedisKey)"("hlen", "HLEN", "key", .not_subscriber).call; pub const hvals = compile.@"(key: RedisKey)"("hvals", "HVALS", "key", .not_subscriber).call; pub const keys = compile.@"(key: RedisKey)"("keys", "KEYS", "key", .not_subscriber).call; +pub const lindex = compile.@"(...strings: string[])"("lindex", "LINDEX", .not_subscriber).call; +pub const linsert = compile.@"(...strings: string[])"("linsert", "LINSERT", .not_subscriber).call; pub const llen = compile.@"(key: RedisKey)"("llen", "LLEN", "key", .not_subscriber).call; +pub const lmove = compile.@"(...strings: string[])"("lmove", "LMOVE", .not_subscriber).call; +pub const lmpop = compile.@"(...strings: string[])"("lmpop", "LMPOP", .not_subscriber).call; pub const lpop = compile.@"(key: RedisKey)"("lpop", "LPOP", "key", .not_subscriber).call; +pub const lpos = compile.@"(...strings: string[])"("lpos", "LPOS", .not_subscriber).call; +pub const lrange = compile.@"(...strings: string[])"("lrange", "LRANGE", .not_subscriber).call; +pub const lrem = compile.@"(...strings: string[])"("lrem", "LREM", .not_subscriber).call; +pub const lset = compile.@"(...strings: string[])"("lset", "LSET", .not_subscriber).call; +pub const ltrim = compile.@"(...strings: string[])"("ltrim", "LTRIM", .not_subscriber).call; pub const persist = compile.@"(key: RedisKey)"("persist", "PERSIST", "key", .not_subscriber).call; pub const pexpire = compile.@"(key: RedisKey, value: RedisValue)"("pexpire", "PEXPIRE", "key", "milliseconds", .not_subscriber).call; pub const pexpireat = compile.@"(key: RedisKey, value: RedisValue)"("pexpireat", "PEXPIREAT", "key", "milliseconds-timestamp", .not_subscriber).call; @@ -654,6 +668,7 @@ pub const pexpiretime = compile.@"(key: RedisKey)"("pexpiretime", "PEXPIRETIME", pub const pttl = compile.@"(key: RedisKey)"("pttl", "PTTL", "key", .not_subscriber).call; pub const randomkey = compile.@"()"("randomkey", "RANDOMKEY", .not_subscriber).call; pub const rpop = compile.@"(key: RedisKey)"("rpop", "RPOP", "key", .not_subscriber).call; +pub const rpoplpush = compile.@"(...strings: string[])"("rpoplpush", "RPOPLPUSH", .not_subscriber).call; pub const scan = compile.@"(...strings: string[])"("scan", "SCAN", .not_subscriber).call; pub const scard = compile.@"(key: RedisKey)"("scard", "SCARD", "key", .not_subscriber).call; pub const strlen = compile.@"(key: RedisKey)"("strlen", "STRLEN", "key", .not_subscriber).call; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 07b43fae1109..a10af8e45852 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -874,6 +874,892 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); }); + describe("List Operations", () => { + test("should get range of elements with LRANGE", async () => { + const redis = ctx.redis; + const key = "lrange-test"; + + // Create a list with multiple elements + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Get full list + const fullList = await redis.lrange(key, 0, -1); + expect(fullList).toEqual(["one", "two", "three"]); + + // Get first two elements + const firstTwo = await redis.lrange(key, 0, 1); + expect(firstTwo).toEqual(["one", "two"]); + + // Get last two elements using negative indexes + const lastTwo = await redis.lrange(key, -2, -1); + expect(lastTwo).toEqual(["two", "three"]); + + // Get middle element + const middle = await redis.lrange(key, 1, 1); + expect(middle).toEqual(["two"]); + + // Out of range should return empty array + const outOfRange = await redis.lrange(key, 10, 20); + expect(outOfRange).toEqual([]); + + // Non-existent key should return empty array + const nonExistent = await redis.lrange("nonexistent-list", 0, -1); + expect(nonExistent).toEqual([]); + }); + + test("should get element at index with LINDEX", async () => { + const redis = ctx.redis; + const key = "lindex-test"; + + // Create a list + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Get element at positive index + const first = await redis.lindex(key, 0); + expect(first).toBe("one"); + + const second = await redis.lindex(key, 1); + expect(second).toBe("two"); + + const third = await redis.lindex(key, 2); + expect(third).toBe("three"); + + // Get element at negative index (counting from end) + const last = await redis.lindex(key, -1); + expect(last).toBe("three"); + + const secondLast = await redis.lindex(key, -2); + expect(secondLast).toBe("two"); + + // Out of range should return null + const outOfRange = await redis.lindex(key, 10); + expect(outOfRange).toBeNull(); + + const outOfRangeNeg = await redis.lindex(key, -10); + expect(outOfRangeNeg).toBeNull(); + + // Non-existent key should return null + const nonExistent = await redis.lindex("nonexistent-list", 0); + expect(nonExistent).toBeNull(); + }); + + test("should set element at index with LSET", async () => { + const redis = ctx.redis; + const key = "lset-test"; + + // Create a list + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Set element at positive index + const result1 = await redis.lset(key, 0, "zero"); + expect(result1).toBe("OK"); + + // Verify the change + const first = await redis.lindex(key, 0); + expect(first).toBe("zero"); + + // Set element at negative index + const result2 = await redis.lset(key, -1, "last"); + expect(result2).toBe("OK"); + + // Verify the change + const last = await redis.lindex(key, -1); + expect(last).toBe("last"); + + // Check full list + const fullList = await redis.lrange(key, 0, -1); + expect(fullList).toEqual(["zero", "two", "last"]); + }); + + test("should handle LSET errors", async () => { + const redis = ctx.redis; + + // Test out of range index on existing list + await redis.lpush("lset-error-test", "value"); + + // Out of range should throw an error + await expect(async () => { + await redis.lset("lset-error-test", 10, "newvalue"); + }).toThrow(); + + // Non-existent key should throw an error + await expect(async () => { + await redis.lset("nonexistent-list", 0, "value"); + }).toThrow(); + + // Wrong type (not a list) should throw an error + await redis.set("string-key", "value"); + await expect(async () => { + await redis.lset("string-key", 0, "value"); + }).toThrow(); + }); + + test("should handle LRANGE with various ranges", async () => { + const redis = ctx.redis; + const key = "lrange-advanced"; + + // Create a longer list + for (let i = 5; i >= 1; i--) { + await redis.lpush(key, String(i)); + } + + // Verify the list: [1, 2, 3, 4, 5] + const fullList = await redis.lrange(key, 0, -1); + expect(fullList).toEqual(["1", "2", "3", "4", "5"]); + + // Test with stop less than start (should return empty) + const invalid = await redis.lrange(key, 3, 1); + expect(invalid).toEqual([]); + + // Test with negative start and positive stop + const mixed = await redis.lrange(key, -3, 4); + expect(mixed).toEqual(["3", "4", "5"]); + + // Test with both negative + const bothNeg = await redis.lrange(key, -4, -2); + expect(bothNeg).toEqual(["2", "3", "4"]); + }); + + test("should handle LINDEX and LSET with numbers", async () => { + const redis = ctx.redis; + const key = "list-numbers"; + + // Push numeric strings + await redis.lpush(key, "100"); + await redis.lpush(key, "200"); + await redis.lpush(key, "300"); + + // Get element + const elem = await redis.lindex(key, 1); + expect(elem).toBe("200"); + + // Set with number (should convert to string) + await redis.lset(key, 1, "250"); + const updated = await redis.lindex(key, 1); + expect(updated).toBe("250"); + }); + + test("should insert element before pivot with LINSERT", async () => { + const redis = ctx.redis; + const key = "linsert-before-test"; + + // Create a list + await redis.lpush(key, "World"); + await redis.lpush(key, "Hello"); + + // Insert before "World" + const result = await redis.linsert(key, "BEFORE", "World", "There"); + expect(result).toBe(3); // List length after insert + + // Verify the list + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["Hello", "There", "World"]); + }); + + test("should insert element after pivot with LINSERT", async () => { + const redis = ctx.redis; + const key = "linsert-after-test"; + + // Create a list + await redis.lpush(key, "World"); + await redis.lpush(key, "Hello"); + + // Insert after "Hello" + const result = await redis.linsert(key, "AFTER", "Hello", "Beautiful"); + expect(result).toBe(3); // List length after insert + + // Verify the list + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["Hello", "Beautiful", "World"]); + }); + + test("should handle LINSERT when pivot not found", async () => { + const redis = ctx.redis; + const key = "linsert-notfound-test"; + + // Create a list + await redis.lpush(key, "value1"); + await redis.lpush(key, "value2"); + + // Try to insert before non-existent pivot + const result = await redis.linsert(key, "BEFORE", "nonexistent", "newvalue"); + expect(result).toBe(-1); // Pivot not found + + // Verify list unchanged + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["value2", "value1"]); + }); + + test("should handle LINSERT on non-existent key", async () => { + const redis = ctx.redis; + + // Try to insert into non-existent list + const result = await redis.linsert("nonexistent-list", "BEFORE", "pivot", "element"); + expect(result).toBe(0); // Key doesn't exist + }); + + test("should remove elements from head with LREM", async () => { + const redis = ctx.redis; + const key = "lrem-positive-test"; + + // Create a list with duplicates + await redis.rpush(key, "hello"); + await redis.rpush(key, "hello"); + await redis.rpush(key, "world"); + await redis.rpush(key, "hello"); + + // Remove first 2 "hello" from head to tail + const result = await redis.lrem(key, 2, "hello"); + expect(result).toBe(2); // Number of elements removed + + // Verify the list + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["world", "hello"]); + }); + + test("should remove elements from tail with LREM", async () => { + const redis = ctx.redis; + const key = "lrem-negative-test"; + + // Create a list with duplicates + await redis.rpush(key, "hello"); + await redis.rpush(key, "world"); + await redis.rpush(key, "hello"); + await redis.rpush(key, "hello"); + + // Remove first 2 "hello" from tail to head + const result = await redis.lrem(key, -2, "hello"); + expect(result).toBe(2); // Number of elements removed + + // Verify the list + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["hello", "world"]); + }); + + test("should remove all occurrences with LREM count=0", async () => { + const redis = ctx.redis; + const key = "lrem-all-test"; + + // Create a list with multiple duplicates + await redis.rpush(key, "hello"); + await redis.rpush(key, "world"); + await redis.rpush(key, "hello"); + await redis.rpush(key, "foo"); + await redis.rpush(key, "hello"); + + // Remove all "hello" + const result = await redis.lrem(key, 0, "hello"); + expect(result).toBe(3); // Number of elements removed + + // Verify the list + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["world", "foo"]); + }); + + test("should handle LREM when element not found", async () => { + const redis = ctx.redis; + const key = "lrem-notfound-test"; + + // Create a list + await redis.rpush(key, "value1"); + await redis.rpush(key, "value2"); + + // Try to remove non-existent element + const result = await redis.lrem(key, 1, "nonexistent"); + expect(result).toBe(0); // No elements removed + + // Verify list unchanged + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["value1", "value2"]); + }); + + test("should trim list to range with LTRIM", async () => { + const redis = ctx.redis; + const key = "ltrim-test"; + + // Create a list + await redis.rpush(key, "one"); + await redis.rpush(key, "two"); + await redis.rpush(key, "three"); + await redis.rpush(key, "four"); + + // Trim to keep only elements 1-2 + const result = await redis.ltrim(key, 1, 2); + expect(result).toBe("OK"); + + // Verify the list + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["two", "three"]); + }); + + test("should handle LTRIM with negative indexes", async () => { + const redis = ctx.redis; + const key = "ltrim-negative-test"; + + // Create a list + await redis.rpush(key, "one"); + await redis.rpush(key, "two"); + await redis.rpush(key, "three"); + await redis.rpush(key, "four"); + await redis.rpush(key, "five"); + + // Trim to keep last 3 elements + const result = await redis.ltrim(key, -3, -1); + expect(result).toBe("OK"); + + // Verify the list + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["three", "four", "five"]); + }); + + test("should handle LTRIM with out of range indexes", async () => { + const redis = ctx.redis; + const key = "ltrim-outofrange-test"; + + // Create a list + await redis.rpush(key, "one"); + await redis.rpush(key, "two"); + await redis.rpush(key, "three"); + + // Trim with large range (should keep all elements) + const result = await redis.ltrim(key, 0, 100); + expect(result).toBe("OK"); + + // Verify the list unchanged + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["one", "two", "three"]); + }); + + test("should empty list with LTRIM when stop < start", async () => { + const redis = ctx.redis; + const key = "ltrim-empty-test"; + + // Create a list + await redis.rpush(key, "one"); + await redis.rpush(key, "two"); + await redis.rpush(key, "three"); + + // Trim with invalid range (stop < start) + const result = await redis.ltrim(key, 2, 0); + expect(result).toBe("OK"); + + // Verify the list is empty + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual([]); + }); + + test("should block and pop element with BLPOP", async () => { + const redis = ctx.redis; + const key = "blpop-test"; + + // Push an element + await redis.lpush(key, "value1"); + + // BLPOP should return immediately + const result = await redis.blpop(key, 0.1); + expect(result).toEqual([key, "value1"]); + + // BLPOP on empty list should timeout and return null + const timeout = await redis.blpop(key, 0.1); + expect(timeout).toBeNull(); + }); + + test("should block and pop element with BRPOP", async () => { + const redis = ctx.redis; + const key = "brpop-test"; + + // Push multiple elements + await redis.lpush(key, "value2"); + await redis.lpush(key, "value1"); + + // BRPOP should return last element + const result = await redis.brpop(key, 0.1); + expect(result).toEqual([key, "value2"]); + + // BRPOP on empty list should timeout and return null + await redis.brpop(key, 0.1); // Pop remaining element + const timeout = await redis.brpop(key, 0.1); + expect(timeout).toBeNull(); + }); + + test("should pop from first non-empty list with BLPOP", async () => { + const redis = ctx.redis; + const key1 = "blpop-list1"; + const key2 = "blpop-list2"; + + // Only push to second list + await redis.lpush(key2, "value2"); + + // Should pop from key2 since key1 is empty + const result = await redis.blpop(key1, key2, 0.1); + expect(result).toEqual([key2, "value2"]); + }); + + test("should pop elements with LMPOP LEFT", async () => { + const redis = ctx.redis; + const key = "lmpop-left-test"; + + // Push elements + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Pop one element from head (LEFT) + const result = await redis.lmpop(1, key, "LEFT"); + expect(result).toEqual([key, ["one"]]); + + // Verify remaining elements + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual(["two", "three"]); + }); + + test("should pop elements with LMPOP RIGHT", async () => { + const redis = ctx.redis; + const key = "lmpop-right-test"; + + // Push elements + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Pop one element from tail (RIGHT) + const result = await redis.lmpop(1, key, "RIGHT"); + expect(result).toEqual([key, ["three"]]); + + // Verify remaining elements + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual(["one", "two"]); + }); + + test("should pop multiple elements with LMPOP COUNT", async () => { + const redis = ctx.redis; + const key = "lmpop-count-test"; + + // Push elements + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Pop 2 elements from head + const result = await redis.lmpop(1, key, "LEFT", "COUNT", 2); + expect(result).toEqual([key, ["one", "two"]]); + + // Verify remaining element + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual(["three"]); + }); + + test("should return null for LMPOP on empty list", async () => { + const redis = ctx.redis; + + // Try to pop from non-existent list + const result = await redis.lmpop(1, "nonexistent-list", "LEFT"); + expect(result).toBeNull(); + }); + + test("should pop from first non-empty list with LMPOP", async () => { + const redis = ctx.redis; + const key1 = "lmpop-empty"; + const key2 = "lmpop-full"; + + // Only push to second list + await redis.lpush(key2, "value"); + + // Should pop from key2 since key1 is empty + const result = await redis.lmpop(2, key1, key2, "LEFT"); + expect(result).toEqual([key2, ["value"]]); + }); + + test("should find position of element with LPOS", async () => { + const redis = ctx.redis; + const key = "lpos-test"; + + // Create a list with some duplicates + await redis.lpush(key, "d"); + await redis.lpush(key, "b"); + await redis.lpush(key, "c"); + await redis.lpush(key, "b"); + await redis.lpush(key, "a"); + // List is now: ["a", "b", "c", "b", "d"] + + // Find first occurrence of "b" + const pos1 = await redis.lpos(key, "b"); + expect(pos1).toBe(1); + + // Find first occurrence of "a" + const pos2 = await redis.lpos(key, "a"); + expect(pos2).toBe(0); + + // Find element at the end + const pos3 = await redis.lpos(key, "d"); + expect(pos3).toBe(4); + + // Non-existent element should return null + const pos4 = await redis.lpos(key, "x"); + expect(pos4).toBeNull(); + }); + + test("should find position with RANK option in LPOS", async () => { + const redis = ctx.redis; + const key = "lpos-rank-test"; + + // Create a list with duplicates + await redis.lpush(key, "b"); + await redis.lpush(key, "a"); + await redis.lpush(key, "b"); + await redis.lpush(key, "a"); + await redis.lpush(key, "b"); + // List is now: ["b", "a", "b", "a", "b"] + + // Find first occurrence (default behavior) + const first = await redis.lpos(key, "b"); + expect(first).toBe(0); + + // Find second occurrence + const second = await redis.lpos(key, "b", "RANK", 2); + expect(second).toBe(2); + + // Find third occurrence + const third = await redis.lpos(key, "b", "RANK", 3); + expect(third).toBe(4); + + // Find with RANK that doesn't exist + const fourth = await redis.lpos(key, "b", "RANK", 4); + expect(fourth).toBeNull(); + + // Find with negative RANK (from tail) + const fromEnd = await redis.lpos(key, "b", "RANK", -1); + expect(fromEnd).toBe(4); + + const fromEnd2 = await redis.lpos(key, "b", "RANK", -2); + expect(fromEnd2).toBe(2); + }); + + test("should find multiple positions with COUNT option in LPOS", async () => { + const redis = ctx.redis; + const key = "lpos-count-test"; + + // Create a list with duplicates + await redis.lpush(key, "c"); + await redis.lpush(key, "b"); + await redis.lpush(key, "b"); + await redis.lpush(key, "a"); + await redis.lpush(key, "b"); + // List is now: ["b", "a", "b", "b", "c"] + + // Find all occurrences (COUNT 0 means all) + const all = await redis.lpos(key, "b", "COUNT", 0); + expect(all).toEqual([0, 2, 3]); + + // Find first 2 occurrences + const first2 = await redis.lpos(key, "b", "COUNT", 2); + expect(first2).toEqual([0, 2]); + + // Find with COUNT greater than actual occurrences + const more = await redis.lpos(key, "b", "COUNT", 10); + expect(more).toEqual([0, 2, 3]); + + // Find non-existent with COUNT + const none = await redis.lpos(key, "x", "COUNT", 5); + expect(none).toEqual([]); + }); + + test("should find position with MAXLEN option in LPOS", async () => { + const redis = ctx.redis; + const key = "lpos-maxlen-test"; + + // Create a longer list + for (let i = 5; i >= 1; i--) { + await redis.lpush(key, String(i)); + } + await redis.lpush(key, "target"); + // List is now: ["target", "1", "2", "3", "4", "5"] + + // Find within first 6 elements (should find it) + const found = await redis.lpos(key, "target", "MAXLEN", 6); + expect(found).toBe(0); + + // Find "5" with MAXLEN that's too short + const notFound = await redis.lpos(key, "5", "MAXLEN", 3); + expect(notFound).toBeNull(); + + // Find "3" with MAXLEN + const found3 = await redis.lpos(key, "3", "MAXLEN", 10); + expect(found3).toBe(3); + }); + + test("should move element from source to destination with LMOVE", async () => { + const redis = ctx.redis; + const source = "lmove-source"; + const dest = "lmove-dest"; + + // Setup source list + await redis.lpush(source, "three"); + await redis.lpush(source, "two"); + await redis.lpush(source, "one"); + // Source: ["one", "two", "three"] + + // Move from LEFT of source to RIGHT of dest + const result1 = await redis.lmove(source, dest, "LEFT", "RIGHT"); + expect(result1).toBe("one"); + + // Verify source and dest + const sourceList1 = await redis.lrange(source, 0, -1); + expect(sourceList1).toEqual(["two", "three"]); + + const destList1 = await redis.lrange(dest, 0, -1); + expect(destList1).toEqual(["one"]); + + // Move from RIGHT of source to LEFT of dest + const result2 = await redis.lmove(source, dest, "RIGHT", "LEFT"); + expect(result2).toBe("three"); + + const sourceList2 = await redis.lrange(source, 0, -1); + expect(sourceList2).toEqual(["two"]); + + const destList2 = await redis.lrange(dest, 0, -1); + expect(destList2).toEqual(["three", "one"]); + }); + + test("should handle all LMOVE direction combinations", async () => { + const redis = ctx.redis; + + // Test LEFT -> LEFT + await redis.lpush("src1", "b", "a"); + const res1 = await redis.lmove("src1", "dst1", "LEFT", "LEFT"); + expect(res1).toBe("a"); + expect(await redis.lrange("dst1", 0, -1)).toEqual(["a"]); + + // Test LEFT -> RIGHT + await redis.lpush("src2", "b", "a"); + const res2 = await redis.lmove("src2", "dst2", "LEFT", "RIGHT"); + expect(res2).toBe("a"); + expect(await redis.lrange("dst2", 0, -1)).toEqual(["a"]); + + // Test RIGHT -> LEFT + await redis.lpush("src3", "b", "a"); + const res3 = await redis.lmove("src3", "dst3", "RIGHT", "LEFT"); + expect(res3).toBe("b"); + expect(await redis.lrange("dst3", 0, -1)).toEqual(["b"]); + + // Test RIGHT -> RIGHT + await redis.lpush("src4", "b", "a"); + const res4 = await redis.lmove("src4", "dst4", "RIGHT", "RIGHT"); + expect(res4).toBe("b"); + expect(await redis.lrange("dst4", 0, -1)).toEqual(["b"]); + }); + + test("should return null for LMOVE on empty source", async () => { + const redis = ctx.redis; + + const result = await redis.lmove("empty-source", "some-dest", "LEFT", "RIGHT"); + expect(result).toBeNull(); + + // Destination should also be empty + const destList = await redis.lrange("some-dest", 0, -1); + expect(destList).toEqual([]); + }); + + test("should handle LMOVE to same list", async () => { + const redis = ctx.redis; + const key = "circular-list"; + + await redis.lpush(key, "c", "b", "a"); + // List: ["a", "b", "c"] + + // Move from LEFT to RIGHT (rotate left) + const result = await redis.lmove(key, key, "LEFT", "RIGHT"); + expect(result).toBe("a"); + expect(await redis.lrange(key, 0, -1)).toEqual(["b", "c", "a"]); + }); + + test("should pop from source and push to dest with RPOPLPUSH", async () => { + const redis = ctx.redis; + const source = "rpoplpush-source"; + const dest = "rpoplpush-dest"; + + // Setup source list + await redis.lpush(source, "three"); + await redis.lpush(source, "two"); + await redis.lpush(source, "one"); + // Source: ["one", "two", "three"] + + // Pop from tail (RIGHT) of source and push to head (LEFT) of dest + const result = await redis.rpoplpush(source, dest); + expect(result).toBe("three"); + + // Verify source and dest + const sourceList = await redis.lrange(source, 0, -1); + expect(sourceList).toEqual(["one", "two"]); + + const destList = await redis.lrange(dest, 0, -1); + expect(destList).toEqual(["three"]); + + // Do it again + const result2 = await redis.rpoplpush(source, dest); + expect(result2).toBe("two"); + + const sourceList2 = await redis.lrange(source, 0, -1); + expect(sourceList2).toEqual(["one"]); + + const destList2 = await redis.lrange(dest, 0, -1); + expect(destList2).toEqual(["two", "three"]); + }); + + test("should return null for RPOPLPUSH on empty source", async () => { + const redis = ctx.redis; + + const result = await redis.rpoplpush("empty-source", "some-dest"); + expect(result).toBeNull(); + }); + + test("should handle RPOPLPUSH to same list (circular)", async () => { + const redis = ctx.redis; + const key = "circular-rpoplpush"; + + await redis.lpush(key, "c", "b", "a"); + // List: ["a", "b", "c"] + + // Pop from tail and push to head (rotate right) + const result = await redis.rpoplpush(key, key); + expect(result).toBe("c"); + expect(await redis.lrange(key, 0, -1)).toEqual(["c", "a", "b"]); + }); + + test("should block and move element with BLMOVE", async () => { + const redis = ctx.redis; + const source = "blmove-source"; + const dest = "blmove-dest"; + + // Push elements to source + await redis.lpush(source, "three"); + await redis.lpush(source, "two"); + await redis.lpush(source, "one"); + + // Move from right of source to left of dest (like BRPOPLPUSH) + const result = await redis.blmove(source, dest, "RIGHT", "LEFT", 0.1); + expect(result).toBe("three"); + + // Verify source has 2 elements + const sourceRemaining = await redis.lrange(source, 0, -1); + expect(sourceRemaining).toEqual(["one", "two"]); + + // Verify dest has 1 element at head + const destElements = await redis.lrange(dest, 0, -1); + expect(destElements).toEqual(["three"]); + + // Move from left of source to right of dest + const result2 = await redis.blmove(source, dest, "LEFT", "RIGHT", 0.1); + expect(result2).toBe("one"); + + // Verify final state + const finalSource = await redis.lrange(source, 0, -1); + expect(finalSource).toEqual(["two"]); + + const finalDest = await redis.lrange(dest, 0, -1); + expect(finalDest).toEqual(["three", "one"]); + }); + + test("should timeout and return null with BLMOVE on empty list", async () => { + const redis = ctx.redis; + + // Try to move from empty list with short timeout + const result = await redis.blmove("empty-source", "dest", "LEFT", "RIGHT", 0.1); + expect(result).toBeNull(); + }); + + test("should block and pop multiple elements with BLMPOP", async () => { + const redis = ctx.redis; + const key = "blmpop-test"; + + // Push elements + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Pop one element from head (LEFT) + const result = await redis.blmpop(0.1, 1, key, "LEFT"); + expect(result).toEqual([key, ["one"]]); + + // Pop 2 elements from tail (RIGHT) with COUNT + const result2 = await redis.blmpop(0.1, 1, key, "RIGHT", "COUNT", 2); + expect(result2).toEqual([key, ["three", "two"]]); + + // List should now be empty + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual([]); + }); + + test("should pop from first non-empty list with BLMPOP", async () => { + const redis = ctx.redis; + const key1 = "blmpop-empty"; + const key2 = "blmpop-full"; + + // Only push to second list + await redis.lpush(key2, "value"); + + // Should pop from key2 since key1 is empty + const result = await redis.blmpop(0.1, 2, key1, key2, "LEFT"); + expect(result).toEqual([key2, ["value"]]); + }); + + test("should timeout and return null with BLMPOP on empty lists", async () => { + const redis = ctx.redis; + + // Try to pop from non-existent lists with short timeout + const result = await redis.blmpop(0.1, 2, "empty-list1", "empty-list2", "LEFT"); + expect(result).toBeNull(); + }); + + test("should block and move element with BRPOPLPUSH", async () => { + const redis = ctx.redis; + const source = "brpoplpush-source"; + const dest = "brpoplpush-dest"; + + // Push elements to source + await redis.lpush(source, "value2"); + await redis.lpush(source, "value1"); + + // Move from tail of source to head of dest + const result = await redis.brpoplpush(source, dest, 0.1); + expect(result).toBe("value2"); + + // Verify source has 1 element + const sourceRemaining = await redis.lrange(source, 0, -1); + expect(sourceRemaining).toEqual(["value1"]); + + // Verify dest has 1 element + const destElements = await redis.lrange(dest, 0, -1); + expect(destElements).toEqual(["value2"]); + + // Move again + const result2 = await redis.brpoplpush(source, dest, 0.1); + expect(result2).toBe("value1"); + + // Source should be empty + const finalSource = await redis.lrange(source, 0, -1); + expect(finalSource).toEqual([]); + + // Dest should have both elements + const finalDest = await redis.lrange(dest, 0, -1); + expect(finalDest).toEqual(["value1", "value2"]); + }); + + test("should timeout and return null with BRPOPLPUSH on empty list", async () => { + const redis = ctx.redis; + + // Try to move from empty list with short timeout + const result = await redis.brpoplpush("empty-source", "dest", 0.1); + expect(result).toBeNull(); + }); + }); + describe("Sorted Set Operations", () => { test("should increment score with ZINCRBY", async () => { const redis = ctx.redis; From 47f906a8a2f71dab0a3f6066926fd912de92c502 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 22:36:13 -0700 Subject: [PATCH 13/45] feat: add all the remaining set operations --- packages/bun-types/redis.d.ts | 78 +++++ src/bun.js/api/valkey.classes.ts | 36 ++ src/valkey/js_valkey.zig | 9 + src/valkey/js_valkey_functions.zig | 9 + test/js/valkey/unit/set-operations.test.ts | 147 +++++++++ test/js/valkey/valkey.test.ts | 366 ++++++++++++++++++++- 6 files changed, 639 insertions(+), 6 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index c6ddc5675e9d..01b7c9388a0f 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -383,6 +383,33 @@ declare module "bun" { */ spop(key: RedisClient.KeyLike): Promise; + /** + * Store the difference of multiple sets in a key + * @param destination The destination key to store the result + * @param key The first set key + * @param keys Additional set keys to subtract from the first set + * @returns Promise that resolves with the number of elements in the resulting set + */ + sdiffstore(destination: RedisClient.KeyLike, key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Check if multiple members are members of a set + * @param key The set key + * @param member The first member to check + * @param members Additional members to check + * @returns Promise that resolves with an array of 1s and 0s indicating membership + */ + smismember(key: RedisClient.KeyLike, member: RedisClient.KeyLike, ...members: RedisClient.KeyLike[]): Promise; + + /** + * Incrementally iterate over a set + * @param key The set key + * @param cursor The cursor value + * @param args Additional SSCAN options (MATCH pattern, COUNT hint) + * @returns Promise that resolves with a tuple [cursor, members[]] + */ + sscan(key: RedisClient.KeyLike, cursor: number | string, ...args: (string | number)[]): Promise<[string, string[]]>; + /** * Increment the integer value of a hash field by the given number * @param key The hash key @@ -906,6 +933,40 @@ declare module "bun" { */ scard(key: RedisClient.KeyLike): Promise; + /** + * Get the difference of multiple sets + * @param key The first set key + * @param keys Additional set keys to subtract from the first set + * @returns Promise that resolves with an array of members in the difference + */ + sdiff(key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Get the intersection of multiple sets + * @param key The first set key + * @param keys Additional set keys to intersect + * @returns Promise that resolves with an array of members in the intersection + */ + sinter(key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Store the intersection of multiple sets in a key + * @param destination The destination key to store the result + * @param key The first set key + * @param keys Additional set keys to intersect + * @returns Promise that resolves with the number of elements in the resulting set + */ + sinterstore(destination: RedisClient.KeyLike, key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Get the cardinality of the intersection of multiple sets + * @param numkeys The number of keys to intersect + * @param key The first set key + * @param args Additional set keys and optional LIMIT argument + * @returns Promise that resolves with the number of elements in the intersection + */ + sintercard(numkeys: number, key: RedisClient.KeyLike, ...args: (RedisClient.KeyLike | "LIMIT" | number)[]): Promise; + /** * Get the length of the value stored in a key * @param key The key to check @@ -914,6 +975,23 @@ declare module "bun" { */ strlen(key: RedisClient.KeyLike): Promise; + /** + * Get the union of multiple sets + * @param key The first set key + * @param keys Additional set keys to union + * @returns Promise that resolves with an array of members in the union + */ + sunion(key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Store the union of multiple sets in a key + * @param destination The destination key to store the result + * @param key The first set key + * @param keys Additional set keys to union + * @returns Promise that resolves with the number of elements in the resulting set + */ + sunionstore(destination: RedisClient.KeyLike, key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + /** * Determine the type of value stored at key * diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index f4d59c94b5b0..f3c2f5ba10f2 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -273,9 +273,45 @@ export default [ scard: { fn: "scard", }, + sdiff: { + fn: "sdiff", + length: 1, + }, + sdiffstore: { + fn: "sdiffstore", + length: 2, + }, + sinter: { + fn: "sinter", + length: 1, + }, + sintercard: { + fn: "sintercard", + length: 1, + }, + sinterstore: { + fn: "sinterstore", + length: 2, + }, + smismember: { + fn: "smismember", + length: 2, + }, + sscan: { + fn: "sscan", + length: 2, + }, strlen: { fn: "strlen", }, + sunion: { + fn: "sunion", + length: 1, + }, + sunionstore: { + fn: "sunionstore", + length: 2, + }, type: { fn: "type", }, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index eb9d69c98fc0..52ca7d540c7b 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1315,6 +1315,11 @@ pub const JSValkeyClient = struct { pub const scan = fns.scan; pub const scard = fns.scard; pub const script = fns.script; + pub const sdiff = fns.sdiff; + pub const sdiffstore = fns.sdiffstore; + pub const sinter = fns.sinter; + pub const sintercard = fns.sintercard; + pub const sinterstore = fns.sinterstore; pub const select = fns.select; pub const set = fns.set; pub const setbit = fns.setbit; @@ -1323,14 +1328,18 @@ pub const JSValkeyClient = struct { pub const setrange = fns.setrange; pub const sismember = fns.sismember; pub const smembers = fns.smembers; + pub const smismember = fns.smismember; pub const smove = fns.smove; pub const spop = fns.spop; pub const spublish = fns.spublish; pub const srandmember = fns.srandmember; pub const srem = fns.srem; + pub const sscan = fns.sscan; pub const strlen = fns.strlen; pub const subscribe = fns.subscribe; pub const substr = fns.substr; + pub const sunion = fns.sunion; + pub const sunionstore = fns.sunionstore; pub const touch = fns.touch; pub const ttl = fns.ttl; pub const unlink = fns.unlink; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 60ba76c8f2a6..3f6c9c7e39ab 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -671,7 +671,16 @@ pub const rpop = compile.@"(key: RedisKey)"("rpop", "RPOP", "key", .not_subscrib pub const rpoplpush = compile.@"(...strings: string[])"("rpoplpush", "RPOPLPUSH", .not_subscriber).call; pub const scan = compile.@"(...strings: string[])"("scan", "SCAN", .not_subscriber).call; pub const scard = compile.@"(key: RedisKey)"("scard", "SCARD", "key", .not_subscriber).call; +pub const sdiff = compile.@"(...strings: string[])"("sdiff", "SDIFF", .not_subscriber).call; +pub const sdiffstore = compile.@"(...strings: string[])"("sdiffstore", "SDIFFSTORE", .not_subscriber).call; +pub const sinter = compile.@"(...strings: string[])"("sinter", "SINTER", .not_subscriber).call; +pub const sintercard = compile.@"(...strings: string[])"("sintercard", "SINTERCARD", .not_subscriber).call; +pub const sinterstore = compile.@"(...strings: string[])"("sinterstore", "SINTERSTORE", .not_subscriber).call; +pub const smismember = compile.@"(...strings: string[])"("smismember", "SMISMEMBER", .not_subscriber).call; +pub const sscan = compile.@"(...strings: string[])"("sscan", "SSCAN", .not_subscriber).call; pub const strlen = compile.@"(key: RedisKey)"("strlen", "STRLEN", "key", .not_subscriber).call; +pub const sunion = compile.@"(...strings: string[])"("sunion", "SUNION", .not_subscriber).call; +pub const sunionstore = compile.@"(...strings: string[])"("sunionstore", "SUNIONSTORE", .not_subscriber).call; pub const @"type" = compile.@"(key: RedisKey)"("type", "TYPE", "key", .not_subscriber).call; pub const zcard = compile.@"(key: RedisKey)"("zcard", "ZCARD", "key", .not_subscriber).call; pub const zcount = compile.@"(...strings: string[])"("zcount", "ZCOUNT", .not_subscriber).call; diff --git a/test/js/valkey/unit/set-operations.test.ts b/test/js/valkey/unit/set-operations.test.ts index 6fabab94fc29..064c7c7abfc4 100644 --- a/test/js/valkey/unit/set-operations.test.ts +++ b/test/js/valkey/unit/set-operations.test.ts @@ -295,6 +295,153 @@ describe.skipIf(!isEnabled)("Valkey: Set Data Type Operations", () => { expect(destMembers).toContain("a"); expect(destMembers).toContain("b"); }); + + test("SUNION command", async () => { + const set1 = ctx.generateKey("sunion-1"); + const set2 = ctx.generateKey("sunion-2"); + const set3 = ctx.generateKey("sunion-3"); + + // Set up test sets with overlapping values + await ctx.redis.send("SADD", [set1, "a", "b", "c"]); + await ctx.redis.send("SADD", [set2, "c", "d", "e"]); + await ctx.redis.send("SADD", [set3, "e", "f", "g"]); + + // Get union of all three sets + const unionResult = await ctx.redis.send("SUNION", [set1, set2, set3]); + expect(Array.isArray(unionResult)).toBe(true); + expect(unionResult.length).toBe(7); // 7 unique elements across all sets + + // Verify all unique elements are present + expect(unionResult).toContain("a"); + expect(unionResult).toContain("b"); + expect(unionResult).toContain("c"); + expect(unionResult).toContain("d"); + expect(unionResult).toContain("e"); + expect(unionResult).toContain("f"); + expect(unionResult).toContain("g"); + }); + + test("SUNION with non-existent keys", async () => { + const existingSet = ctx.generateKey("sunion-existing"); + const nonExistentSet = ctx.generateKey("sunion-nonexistent"); + + await ctx.redis.send("SADD", [existingSet, "a", "b"]); + + // Union with non-existent key should treat it as empty set + const unionResult = await ctx.redis.send("SUNION", [existingSet, nonExistentSet]); + expect(Array.isArray(unionResult)).toBe(true); + expect(unionResult.length).toBe(2); + expect(unionResult).toContain("a"); + expect(unionResult).toContain("b"); + }); + + test("SUNIONSTORE command", async () => { + const set1 = ctx.generateKey("sunionstore-1"); + const set2 = ctx.generateKey("sunionstore-2"); + const destSet = ctx.generateKey("sunionstore-dest"); + + // Set up test sets + await ctx.redis.send("SADD", [set1, "a", "b", "c"]); + await ctx.redis.send("SADD", [set2, "c", "d", "e"]); + + // Store union result + const storeResult = await ctx.redis.send("SUNIONSTORE", [destSet, set1, set2]); + expectType(storeResult, "number"); + expect(storeResult).toBe(5); // 5 unique members + + // Verify destination set + const destMembers = await ctx.redis.smembers(destSet); + expect(Array.isArray(destMembers)).toBe(true); + expect(destMembers.length).toBe(5); + expect(destMembers).toContain("a"); + expect(destMembers).toContain("b"); + expect(destMembers).toContain("c"); + expect(destMembers).toContain("d"); + expect(destMembers).toContain("e"); + }); + + test("SUNIONSTORE overwrites destination key", async () => { + const set1 = ctx.generateKey("sunionstore-overwrite-1"); + const destSet = ctx.generateKey("sunionstore-overwrite-dest"); + + // Pre-populate destination with different values + await ctx.redis.send("SADD", [destSet, "old1", "old2"]); + await ctx.redis.send("SADD", [set1, "new1", "new2"]); + + // Store union (should overwrite) + const storeResult = await ctx.redis.send("SUNIONSTORE", [destSet, set1]); + expect(storeResult).toBe(2); + + // Verify destination was overwritten + const destMembers = await ctx.redis.smembers(destSet); + expect(destMembers.length).toBe(2); + expect(destMembers).toContain("new1"); + expect(destMembers).toContain("new2"); + expect(destMembers).not.toContain("old1"); + expect(destMembers).not.toContain("old2"); + }); + + test("SDIFF command", async () => { + const set1 = ctx.generateKey("sdiff-1"); + const set2 = ctx.generateKey("sdiff-2"); + const set3 = ctx.generateKey("sdiff-3"); + + // Set up test sets + await ctx.redis.send("SADD", [set1, "a", "b", "c", "d"]); + await ctx.redis.send("SADD", [set2, "c", "d"]); + await ctx.redis.send("SADD", [set3, "d", "e"]); + + // Get difference (elements in set1 not in set2 and set3) + const diffResult = await ctx.redis.send("SDIFF", [set1, set2, set3]); + expect(Array.isArray(diffResult)).toBe(true); + expect(diffResult.length).toBe(2); + expect(diffResult).toContain("a"); + expect(diffResult).toContain("b"); + // c and d should not be present as they exist in set2/set3 + expect(diffResult).not.toContain("c"); + expect(diffResult).not.toContain("d"); + }); + + test("SDIFF with single set", async () => { + const set1 = ctx.generateKey("sdiff-single"); + + await ctx.redis.send("SADD", [set1, "a", "b", "c"]); + + // Difference with no other sets should return all elements + const diffResult = await ctx.redis.send("SDIFF", [set1]); + expect(Array.isArray(diffResult)).toBe(true); + expect(diffResult.length).toBe(3); + expect(diffResult).toContain("a"); + expect(diffResult).toContain("b"); + expect(diffResult).toContain("c"); + }); + + test("SDIFF with non-existent keys", async () => { + const set1 = ctx.generateKey("sdiff-existing"); + const nonExistentSet = ctx.generateKey("sdiff-nonexistent"); + + await ctx.redis.send("SADD", [set1, "a", "b", "c"]); + + // Difference with non-existent key (treated as empty set) + const diffResult = await ctx.redis.send("SDIFF", [set1, nonExistentSet]); + expect(Array.isArray(diffResult)).toBe(true); + expect(diffResult.length).toBe(3); + expect(diffResult).toContain("a"); + expect(diffResult).toContain("b"); + expect(diffResult).toContain("c"); + }); + + test("SDIFF error handling - wrong type", async () => { + const stringKey = ctx.generateKey("sdiff-string"); + const setKey = ctx.generateKey("sdiff-set"); + + // Set up a string key (not a set) + await ctx.redis.set(stringKey, "not a set"); + await ctx.redis.send("SADD", [setKey, "a", "b"]); + + // Should throw error for wrong type + await expect(ctx.redis.send("SDIFF", [stringKey, setKey])).rejects.toThrowErrorMatchingInlineSnapshot(); + }); }); describe("Scanning Operations", () => { diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index a10af8e45852..d820ceb3d0f0 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -984,18 +984,18 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.lpush("lset-error-test", "value"); // Out of range should throw an error - await expect(async () => { + expect(async () => { await redis.lset("lset-error-test", 10, "newvalue"); }).toThrow(); // Non-existent key should throw an error - await expect(async () => { + expect(async () => { await redis.lset("nonexistent-list", 0, "value"); }).toThrow(); // Wrong type (not a list) should throw an error await redis.set("string-key", "value"); - await expect(async () => { + expect(async () => { await redis.lset("string-key", 0, "value"); }).toThrow(); }); @@ -1760,6 +1760,362 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); }); + describe("Set Operations", () => { + test("should return intersection of two sets with SINTER", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + // Create two sets with some overlapping members + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + // Get intersection + const result = await redis.sinter(key1, key2); + expect(result.sort()).toEqual(["b", "c"]); + }); + + test("should return intersection of multiple sets with SINTER", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + const key3 = "set3"; + + // Create three sets with one common member + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + await redis.sadd(key3, "c"); + await redis.sadd(key3, "d"); + await redis.sadd(key3, "e"); + + // Get intersection of all three + const result = await redis.sinter(key1, key2, key3); + expect(result).toEqual(["c"]); + }); + + test("should return empty array when sets have no intersection with SINTER", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + const result = await redis.sinter(key1, key2); + expect(result).toEqual([]); + }); + + test("should return empty array when one set does not exist with SINTER", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "nonexistent"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + + const result = await redis.sinter(key1, key2); + expect(result).toEqual([]); + }); + + test("should store intersection in destination with SINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + const dest = "dest-set"; + + // Create two sets with some overlapping members + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + // Store intersection + const count = await redis.sinterstore(dest, key1, key2); + expect(count).toBe(2); + + // Verify destination has the intersection + const members = await redis.smembers(dest); + expect(members.sort()).toEqual(["b", "c"]); + }); + + test("should overwrite existing destination with SINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + const dest = "dest-set"; + + // Create destination with initial data + await redis.sadd(dest, "old"); + await redis.sadd(dest, "data"); + + // Create two sets + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + + // Store intersection (should overwrite destination) + const count = await redis.sinterstore(dest, key1, key2); + expect(count).toBe(1); + + // Verify destination only has the intersection + const members = await redis.smembers(dest); + expect(members).toEqual(["b"]); + }); + + test("should return 0 when storing empty intersection with SINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + const dest = "dest-set"; + + await redis.sadd(key1, "a"); + await redis.sadd(key2, "b"); + + const count = await redis.sinterstore(dest, key1, key2); + expect(count).toBe(0); + + // Destination should be empty + const members = await redis.smembers(dest); + expect(members).toEqual([]); + }); + + test("should return cardinality of intersection with SINTERCARD", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + // SINTERCARD requires numkeys as first argument + const count = await redis.sintercard(2, key1, key2); + expect(count).toBe(2); + }); + + test("should return 0 for empty intersection with SINTERCARD", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + await redis.sadd(key1, "a"); + await redis.sadd(key2, "b"); + + const count = await redis.sintercard(2, key1, key2); + expect(count).toBe(0); + }); + + test("should support LIMIT option with SINTERCARD", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key1, "d"); + await redis.sadd(key2, "a"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + // LIMIT stops counting after reaching the specified number + const count = await redis.sintercard(2, key1, key2, "LIMIT", 2); + expect(count).toBe(2); + }); + + test("should throw error when SINTER receives no keys", async () => { + const redis = ctx.redis; + + expect(async () => { + // @ts-expect-error no args + await redis.sinter(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sinter' command"`); + }); + + test("should throw error when SINTERSTORE receives no keys", async () => { + const redis = ctx.redis; + + expect(async () => { + // @ts-expect-error no args + await redis.sinterstore(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sinterstore' command"`); + }); + + test("should throw error when SINTERCARD receives no keys", async () => { + const redis = ctx.redis; + + expect(async () => { + // @ts-expect-error no args + await redis.sintercard(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sintercard' command"`); + }); + + test("should store set difference with SDIFFSTORE", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + const key3 = "set3"; + const dest = "diff-result"; + + // Set up test sets + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key1, "d"); + + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + + await redis.sadd(key3, "d"); + + // Store difference between key1 and key2 + const count1 = await redis.sdiffstore(dest, key1, key2); + expect(count1).toBe(2); // a, d + + const members1 = await redis.smembers(dest); + expect(members1.sort()).toEqual(["a", "d"]); + + // Store difference between key1, key2, and key3 + const count2 = await redis.sdiffstore(dest, key1, key2, key3); + expect(count2).toBe(1); // only a + + const members2 = await redis.smembers(dest); + expect(members2).toEqual(["a"]); + }); + + test("should throw error with SDIFFSTORE on invalid arguments", async () => { + const redis = ctx.redis; + + expect(async () => { + await (redis as any).sdiffstore(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sdiffstore' command"`); + }); + + test("should check multiple members with SMISMEMBER", async () => { + const redis = ctx.redis; + const key = "test-set"; + + // Add some members + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); + + // Check which members exist + const result = await redis.smismember(key, "a", "b", "d", "e"); + expect(result).toEqual([1, 1, 0, 0]); // a and b exist, d and e don't + + // Check single member + const result2 = await redis.smismember(key, "c"); + expect(result2).toEqual([1]); + + // Check on non-existent set + const result3 = await redis.smismember("nonexistent", "a", "b"); + expect(result3).toEqual([0, 0]); + }); + + test("should throw error with SMISMEMBER on invalid arguments", async () => { + const redis = ctx.redis; + + expect(async () => { + await (redis as any).smismember(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'smismember' command"`); + }); + + test("should scan set members with SSCAN", async () => { + const redis = ctx.redis; + const key = "scan-set"; + + // Add multiple members + for (let i = 0; i < 20; i++) { + await redis.sadd(key, `member${i}`); + } + + // Scan the set + let cursor = "0"; + const allMembers: string[] = []; + + do { + const [nextCursor, members] = await redis.sscan(key, cursor); + allMembers.push(...members); + cursor = nextCursor; + } while (cursor !== "0"); + + // Should have scanned all 20 members + expect(allMembers.length).toBe(20); + expect(new Set(allMembers).size).toBe(20); // All unique + + // Verify all members exist + for (let i = 0; i < 20; i++) { + expect(allMembers).toContain(`member${i}`); + } + }); + + test("should scan set with MATCH pattern using SSCAN", async () => { + const redis = ctx.redis; + const key = "scan-pattern-set"; + + // Add members with different patterns + await redis.sadd(key, "user:1"); + await redis.sadd(key, "user:2"); + await redis.sadd(key, "user:3"); + await redis.sadd(key, "admin:1"); + await redis.sadd(key, "admin:2"); + + // Scan with MATCH pattern + const [cursor, members] = await redis.sscan(key, "0", "MATCH", "user:*"); + + // Should only return user members (or could be empty if not in first batch) + // Due to cursor-based scanning, we might need multiple iterations + let allUserMembers: string[] = [...members]; + let scanCursor = cursor; + + while (scanCursor !== "0") { + const [nextCursor, nextMembers] = await redis.sscan(key, scanCursor, "MATCH", "user:*"); + allUserMembers.push(...nextMembers); + scanCursor = nextCursor; + } + + // Filter to only user members + const userMembers = allUserMembers.filter(m => m.startsWith("user:")); + expect(userMembers.length).toBeGreaterThanOrEqual(0); // Could be 0 to 3 depending on scan + }); + + test("should scan empty set with SSCAN", async () => { + const redis = ctx.redis; + const key = "empty-scan-set"; + + // Scan empty set + const [cursor, members] = await redis.sscan(key, "0"); + expect(cursor).toBe("0"); + expect(members).toEqual([]); + }); + + test("should throw error with SSCAN on invalid arguments", async () => { + const redis = ctx.redis; + + expect(async () => { + await (redis as any).sscan(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sscan' command"`); + }); + }); + describe("Sorted Set Operations", () => { test("should increment score with ZINCRBY", async () => { const redis = ctx.redis; @@ -3508,9 +3864,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { await redis.bzmpop(1, {} as any, "key1", "MIN"); - }).toThrowErrorMatchingInlineSnapshot( - `"Expected additional arguments to be a string or buffer for 'bzmpop'."`, - ); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'bzmpop'."`); }); test("should block and pop lowest score with BZPOPMIN", async () => { From 1811057e00760178832b1e74cb5656437462c828 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 22:44:42 -0700 Subject: [PATCH 14/45] remove duplicated set operations --- test/js/valkey/unit/set-operations.test.ts | 496 --------------------- 1 file changed, 496 deletions(-) delete mode 100644 test/js/valkey/unit/set-operations.test.ts diff --git a/test/js/valkey/unit/set-operations.test.ts b/test/js/valkey/unit/set-operations.test.ts deleted file mode 100644 index 064c7c7abfc4..000000000000 --- a/test/js/valkey/unit/set-operations.test.ts +++ /dev/null @@ -1,496 +0,0 @@ -import { beforeEach, describe, expect, test } from "bun:test"; -import { ConnectionType, createClient, ctx, expectType, isEnabled } from "../test-utils"; - -/** - * Test suite covering Redis set operations - * - Basic operations (SADD, SREM, SISMEMBER) - * - Set retrieval (SMEMBERS, SCARD) - * - Set manipulation (SPOP, SRANDMEMBER) - * - Set operations (SUNION, SINTER, SDIFF) - */ -describe.skipIf(!isEnabled)("Valkey: Set Data Type Operations", () => { - beforeEach(() => { - if (ctx.redis?.connected) { - ctx.redis.close?.(); - } - ctx.redis = createClient(ConnectionType.TCP); - }); - - describe("Basic Set Operations", () => { - test("SADD and SISMEMBER commands", async () => { - const key = ctx.generateKey("set-test"); - - // Add a single member - const singleAddResult = await ctx.redis.sadd(key, "member1"); - console.log("singleAddResult", singleAddResult); - expectType(singleAddResult, "number"); - expect(singleAddResult).toBe(1); // 1 new member added - - // Add multiple members using sendCommand - const multiAddResult = await ctx.redis.send("SADD", [key, "member2", "member3", "member1"]); - expectType(multiAddResult, "number"); - expect(multiAddResult).toBe(2); // 2 new members added, 1 duplicate ignored - - // Check if member exists - const isFirstMember = await ctx.redis.sismember(key, "member1"); - expect(isFirstMember).toBe(true); - - // Check if non-existent member exists - const isNonMember = await ctx.redis.sismember(key, "nonexistent"); - expect(isNonMember).toBe(false); - }); - - test("SREM command", async () => { - const key = ctx.generateKey("srem-test"); - - // Add multiple members - await ctx.redis.send("SADD", [key, "member1", "member2", "member3", "member4"]); - - // Remove a single member - const singleRemResult = await ctx.redis.srem(key, "member1"); - expectType(singleRemResult, "number"); - expect(singleRemResult).toBe(1); // 1 member removed - - // Remove multiple members using sendCommand - const multiRemResult = await ctx.redis.send("SREM", [key, "member2", "member3", "nonexistent"]); - expectType(multiRemResult, "number"); - expect(multiRemResult).toBe(2); // 2 members removed, non-existent member ignored - - // Verify only member4 remains - const members = await ctx.redis.smembers(key); - expect(Array.isArray(members)).toBe(true); - expect(members.length).toBe(1); - expect(members[0]).toBe("member4"); - }); - - test("SMEMBERS command", async () => { - const key = ctx.generateKey("smembers-test"); - - // Add members one at a time using direct sadd method - await ctx.redis.sadd(key, "apple"); - await ctx.redis.sadd(key, "banana"); - await ctx.redis.sadd(key, "cherry"); - - // Get all members using direct smembers method - const members = await ctx.redis.smembers(key); - expect(Array.isArray(members)).toBe(true); - - // Sort for consistent snapshot since set members can come in any order - const sortedMembers = [...members].sort(); - expect(sortedMembers).toMatchInlineSnapshot(` - [ - "apple", - "banana", - "cherry", - ] - `); - }); - - test("SCARD command", async () => { - const key = ctx.generateKey("scard-test"); - - // Add members - using direct sadd method for first item, then send for multiple - await ctx.redis.sadd(key, "item1"); - await ctx.redis.send("SADD", [key, "item2", "item3", "item4"]); - - // Get set cardinality (size) - // TODO: When a direct scard method is implemented, use that instead - const size = await ctx.redis.send("SCARD", [key]); - expectType(size, "number"); - expect(size).toMatchInlineSnapshot(`4`); - - // Remove some members - using direct srem method for first item, then send for second - await ctx.redis.srem(key, "item1"); - await ctx.redis.send("SREM", [key, "item2"]); - - // Check size again - const updatedSize = await ctx.redis.send("SCARD", [key]); - expectType(updatedSize, "number"); - expect(updatedSize).toMatchInlineSnapshot(`2`); - }); - }); - - describe("Set Manipulation", () => { - test("SPOP command", async () => { - const key = ctx.generateKey("spop-test"); - - // Add members - using send for multiple values - // TODO: When a SADD method that supports multiple values is added, use that instead - await ctx.redis.send("SADD", [key, "red", "green", "blue", "yellow", "purple"]); - - // Pop a single member - using direct spop method - const popResult = await ctx.redis.spop(key); - expect(popResult).toBeDefined(); - expect(typeof popResult).toBe("string"); - - // Pop multiple members - // TODO: When SPOP method that supports count parameter is added, use that instead - const multiPopResult = await ctx.redis.send("SPOP", [key, "2"]); - expect(Array.isArray(multiPopResult)).toBe(true); - expect(multiPopResult.length).toMatchInlineSnapshot(`2`); - - // Verify remaining members - // TODO: When a direct scard method is added, use that instead - const remainingCount = await ctx.redis.send("SCARD", [key]); - expectType(remainingCount, "number"); - expect(remainingCount).toMatchInlineSnapshot(`2`); // 5 original - 1 - 2 = 2 remaining - }); - - test("SRANDMEMBER command", async () => { - const key = ctx.generateKey("srandmember-test"); - - // Add members - first with direct sadd, then with send for remaining - await ctx.redis.sadd(key, "one"); - await ctx.redis.send("SADD", [key, "two", "three", "four", "five"]); - - // Get a random member - using direct srandmember method - const randResult = await ctx.redis.srandmember(key); - expect(randResult).toBeDefined(); - expect(typeof randResult).toBe("string"); - - // Get multiple random members - // TODO: When srandmember method with count parameter is added, use that instead - const multiRandResult = await ctx.redis.send("SRANDMEMBER", [key, "3"]); - expect(Array.isArray(multiRandResult)).toBe(true); - expect(multiRandResult.length).toMatchInlineSnapshot(`3`); - - // Verify set is unchanged - const count = await ctx.redis.send("SCARD", [key]); - expectType(count, "number"); - expect(count).toMatchInlineSnapshot(`5`); // All members still present unlike SPOP - }); - - test("SMOVE command", async () => { - const sourceKey = ctx.generateKey("smove-source"); - const destinationKey = ctx.generateKey("smove-dest"); - - // Set up source and destination sets - await ctx.redis.send("SADD", [sourceKey, "a", "b", "c"]); - await ctx.redis.send("SADD", [destinationKey, "c", "d", "e"]); - - // Move a member from source to destination - const moveResult = await ctx.redis.send("SMOVE", [sourceKey, destinationKey, "b"]); - expectType(moveResult, "number"); - expect(moveResult).toBe(1); // 1 indicates success - - // Try to move a non-existent member - const failedMoveResult = await ctx.redis.send("SMOVE", [sourceKey, destinationKey, "z"]); - expectType(failedMoveResult, "number"); - expect(failedMoveResult).toBe(0); // 0 indicates failure - - // Verify source set (should have "a" and "c" left) - const sourceMembers = await ctx.redis.smembers(sourceKey); - expect(Array.isArray(sourceMembers)).toBe(true); - expect(sourceMembers.length).toBe(2); - expect(sourceMembers).toContain("a"); - expect(sourceMembers).toContain("c"); - expect(sourceMembers).not.toContain("b"); - - // Verify destination set (should have "b", "c", "d", "e") - const destMembers = await ctx.redis.smembers(destinationKey); - expect(Array.isArray(destMembers)).toBe(true); - expect(destMembers.length).toBe(4); - expect(destMembers).toContain("b"); - expect(destMembers).toContain("c"); - expect(destMembers).toContain("d"); - expect(destMembers).toContain("e"); - }); - }); - - describe("Set Operations", () => { - test("SUNION and SUNIONSTORE commands", async () => { - const set1 = ctx.generateKey("sunion-1"); - const set2 = ctx.generateKey("sunion-2"); - const set3 = ctx.generateKey("sunion-3"); - const destSet = ctx.generateKey("sunion-dest"); - - // Set up test sets - await ctx.redis.send("SADD", [set1, "a", "b", "c"]); - await ctx.redis.send("SADD", [set2, "c", "d", "e"]); - await ctx.redis.send("SADD", [set3, "e", "f", "g"]); - - // Get union of two sets - const unionResult = await ctx.redis.send("SUNION", [set1, set2]); - expect(Array.isArray(unionResult)).toBe(true); - expect(unionResult.length).toBe(5); - expect(unionResult).toContain("a"); - expect(unionResult).toContain("b"); - expect(unionResult).toContain("c"); - expect(unionResult).toContain("d"); - expect(unionResult).toContain("e"); - - // Store union of three sets - const storeResult = await ctx.redis.send("SUNIONSTORE", [destSet, set1, set2, set3]); - expectType(storeResult, "number"); - expect(storeResult).toBe(7); // 7 unique members across all sets - - // Verify destination set - const destMembers = await ctx.redis.smembers(destSet); - expect(Array.isArray(destMembers)).toBe(true); - expect(destMembers.length).toBe(7); - expect(destMembers).toContain("a"); - expect(destMembers).toContain("b"); - expect(destMembers).toContain("c"); - expect(destMembers).toContain("d"); - expect(destMembers).toContain("e"); - expect(destMembers).toContain("f"); - expect(destMembers).toContain("g"); - }); - - test("SINTER and SINTERSTORE commands", async () => { - const set1 = ctx.generateKey("sinter-1"); - const set2 = ctx.generateKey("sinter-2"); - const set3 = ctx.generateKey("sinter-3"); - const destSet = ctx.generateKey("sinter-dest"); - - // Set up test sets - await ctx.redis.send("SADD", [set1, "a", "b", "c", "d"]); - await ctx.redis.send("SADD", [set2, "c", "d", "e"]); - await ctx.redis.send("SADD", [set3, "a", "c", "e"]); - - // Get intersection of two sets - const interResult = await ctx.redis.send("SINTER", [set1, set2]); - expect(Array.isArray(interResult)).toBe(true); - expect(interResult.length).toBe(2); - expect(interResult).toContain("c"); - expect(interResult).toContain("d"); - - // Store intersection of three sets - const storeResult = await ctx.redis.send("SINTERSTORE", [destSet, set1, set2, set3]); - expectType(storeResult, "number"); - expect(storeResult).toBe(1); // Only "c" is in all three sets - - // Verify destination set - const destMembers = await ctx.redis.smembers(destSet); - expect(Array.isArray(destMembers)).toBe(true); - expect(destMembers.length).toBe(1); - expect(destMembers[0]).toBe("c"); - }); - - test("SDIFF and SDIFFSTORE commands", async () => { - const set1 = ctx.generateKey("sdiff-1"); - const set2 = ctx.generateKey("sdiff-2"); - const destSet = ctx.generateKey("sdiff-dest"); - - // Set up test sets - await ctx.redis.send("SADD", [set1, "a", "b", "c", "d"]); - await ctx.redis.send("SADD", [set2, "c", "d", "e"]); - - // Get difference (elements in set1 that aren't in set2) - const diffResult = await ctx.redis.send("SDIFF", [set1, set2]); - expect(Array.isArray(diffResult)).toBe(true); - expect(diffResult.length).toBe(2); - expect(diffResult).toContain("a"); - expect(diffResult).toContain("b"); - - // Store difference - const storeResult = await ctx.redis.send("SDIFFSTORE", [destSet, set1, set2]); - expectType(storeResult, "number"); - expect(storeResult).toBe(2); // "a" and "b" are only in set1 - - // Verify destination set - const destMembers = await ctx.redis.smembers(destSet); - expect(Array.isArray(destMembers)).toBe(true); - expect(destMembers.length).toBe(2); - expect(destMembers).toContain("a"); - expect(destMembers).toContain("b"); - }); - - test("SUNION command", async () => { - const set1 = ctx.generateKey("sunion-1"); - const set2 = ctx.generateKey("sunion-2"); - const set3 = ctx.generateKey("sunion-3"); - - // Set up test sets with overlapping values - await ctx.redis.send("SADD", [set1, "a", "b", "c"]); - await ctx.redis.send("SADD", [set2, "c", "d", "e"]); - await ctx.redis.send("SADD", [set3, "e", "f", "g"]); - - // Get union of all three sets - const unionResult = await ctx.redis.send("SUNION", [set1, set2, set3]); - expect(Array.isArray(unionResult)).toBe(true); - expect(unionResult.length).toBe(7); // 7 unique elements across all sets - - // Verify all unique elements are present - expect(unionResult).toContain("a"); - expect(unionResult).toContain("b"); - expect(unionResult).toContain("c"); - expect(unionResult).toContain("d"); - expect(unionResult).toContain("e"); - expect(unionResult).toContain("f"); - expect(unionResult).toContain("g"); - }); - - test("SUNION with non-existent keys", async () => { - const existingSet = ctx.generateKey("sunion-existing"); - const nonExistentSet = ctx.generateKey("sunion-nonexistent"); - - await ctx.redis.send("SADD", [existingSet, "a", "b"]); - - // Union with non-existent key should treat it as empty set - const unionResult = await ctx.redis.send("SUNION", [existingSet, nonExistentSet]); - expect(Array.isArray(unionResult)).toBe(true); - expect(unionResult.length).toBe(2); - expect(unionResult).toContain("a"); - expect(unionResult).toContain("b"); - }); - - test("SUNIONSTORE command", async () => { - const set1 = ctx.generateKey("sunionstore-1"); - const set2 = ctx.generateKey("sunionstore-2"); - const destSet = ctx.generateKey("sunionstore-dest"); - - // Set up test sets - await ctx.redis.send("SADD", [set1, "a", "b", "c"]); - await ctx.redis.send("SADD", [set2, "c", "d", "e"]); - - // Store union result - const storeResult = await ctx.redis.send("SUNIONSTORE", [destSet, set1, set2]); - expectType(storeResult, "number"); - expect(storeResult).toBe(5); // 5 unique members - - // Verify destination set - const destMembers = await ctx.redis.smembers(destSet); - expect(Array.isArray(destMembers)).toBe(true); - expect(destMembers.length).toBe(5); - expect(destMembers).toContain("a"); - expect(destMembers).toContain("b"); - expect(destMembers).toContain("c"); - expect(destMembers).toContain("d"); - expect(destMembers).toContain("e"); - }); - - test("SUNIONSTORE overwrites destination key", async () => { - const set1 = ctx.generateKey("sunionstore-overwrite-1"); - const destSet = ctx.generateKey("sunionstore-overwrite-dest"); - - // Pre-populate destination with different values - await ctx.redis.send("SADD", [destSet, "old1", "old2"]); - await ctx.redis.send("SADD", [set1, "new1", "new2"]); - - // Store union (should overwrite) - const storeResult = await ctx.redis.send("SUNIONSTORE", [destSet, set1]); - expect(storeResult).toBe(2); - - // Verify destination was overwritten - const destMembers = await ctx.redis.smembers(destSet); - expect(destMembers.length).toBe(2); - expect(destMembers).toContain("new1"); - expect(destMembers).toContain("new2"); - expect(destMembers).not.toContain("old1"); - expect(destMembers).not.toContain("old2"); - }); - - test("SDIFF command", async () => { - const set1 = ctx.generateKey("sdiff-1"); - const set2 = ctx.generateKey("sdiff-2"); - const set3 = ctx.generateKey("sdiff-3"); - - // Set up test sets - await ctx.redis.send("SADD", [set1, "a", "b", "c", "d"]); - await ctx.redis.send("SADD", [set2, "c", "d"]); - await ctx.redis.send("SADD", [set3, "d", "e"]); - - // Get difference (elements in set1 not in set2 and set3) - const diffResult = await ctx.redis.send("SDIFF", [set1, set2, set3]); - expect(Array.isArray(diffResult)).toBe(true); - expect(diffResult.length).toBe(2); - expect(diffResult).toContain("a"); - expect(diffResult).toContain("b"); - // c and d should not be present as they exist in set2/set3 - expect(diffResult).not.toContain("c"); - expect(diffResult).not.toContain("d"); - }); - - test("SDIFF with single set", async () => { - const set1 = ctx.generateKey("sdiff-single"); - - await ctx.redis.send("SADD", [set1, "a", "b", "c"]); - - // Difference with no other sets should return all elements - const diffResult = await ctx.redis.send("SDIFF", [set1]); - expect(Array.isArray(diffResult)).toBe(true); - expect(diffResult.length).toBe(3); - expect(diffResult).toContain("a"); - expect(diffResult).toContain("b"); - expect(diffResult).toContain("c"); - }); - - test("SDIFF with non-existent keys", async () => { - const set1 = ctx.generateKey("sdiff-existing"); - const nonExistentSet = ctx.generateKey("sdiff-nonexistent"); - - await ctx.redis.send("SADD", [set1, "a", "b", "c"]); - - // Difference with non-existent key (treated as empty set) - const diffResult = await ctx.redis.send("SDIFF", [set1, nonExistentSet]); - expect(Array.isArray(diffResult)).toBe(true); - expect(diffResult.length).toBe(3); - expect(diffResult).toContain("a"); - expect(diffResult).toContain("b"); - expect(diffResult).toContain("c"); - }); - - test("SDIFF error handling - wrong type", async () => { - const stringKey = ctx.generateKey("sdiff-string"); - const setKey = ctx.generateKey("sdiff-set"); - - // Set up a string key (not a set) - await ctx.redis.set(stringKey, "not a set"); - await ctx.redis.send("SADD", [setKey, "a", "b"]); - - // Should throw error for wrong type - await expect(ctx.redis.send("SDIFF", [stringKey, setKey])).rejects.toThrowErrorMatchingInlineSnapshot(); - }); - }); - - describe("Scanning Operations", () => { - test("SSCAN command", async () => { - const key = ctx.generateKey("sscan-test"); - - // Create a set with many members - const memberCount = 100; - const members = []; - for (let i = 0; i < memberCount; i++) { - members.push(`member:${i}`); - } - - await ctx.redis.send("SADD", [key, ...members]); - - // Use SSCAN to iterate through members - const scanResult = await ctx.redis.send("SSCAN", [key, "0", "COUNT", "20"]); - expect(Array.isArray(scanResult)).toBe(true); - expect(scanResult.length).toBe(2); - - const cursor = scanResult[0]; - const items = scanResult[1]; - - // Cursor should be either "0" (done) or a string number - expect(typeof cursor).toBe("string"); - - // Items should be an array of members - expect(Array.isArray(items)).toBe(true); - - // All results should match our expected pattern - for (const item of items) { - expect(item.startsWith("member:")).toBe(true); - } - - // Verify MATCH pattern works - const patternResult = await ctx.redis.send("SSCAN", [key, "0", "MATCH", "member:1*", "COUNT", "1000"]); - expect(Array.isArray(patternResult)).toBe(true); - expect(patternResult.length).toBe(2); - - const patternItems = patternResult[1]; - expect(Array.isArray(patternItems)).toBe(true); - - // Should return only members that match the pattern (member:1, member:10-19, etc) - // There should be at least "member:1" and "member:10" through "member:19" - expect(patternItems.length).toBeGreaterThan(0); - - for (const item of patternItems) { - expect(item.startsWith("member:1")).toBe(true); - } - }); - }); -}); From 413ed5520346327341b00bc467e7dae6149df9ce Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 22:50:50 -0700 Subject: [PATCH 15/45] deprecated RedisClient#hmset --- redis.md | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 redis.md diff --git a/redis.md b/redis.md new file mode 100644 index 000000000000..601a00ba76cc --- /dev/null +++ b/redis.md @@ -0,0 +1,134 @@ +# Redis Commands Implementation Progress + +## Core String Operations (11 commands) + +- [x] GETRANGE +- [x] SETRANGE +- [x] SETEX +- [x] PSETEX +- [x] GETBIT +- [x] SETBIT +- [x] INCRBY +- [x] INCRBYFLOAT +- [x] DECRBY +- [x] MSET +- [x] MSETNX + +## List Operations (15 commands) + +- [x] LRANGE +- [x] LINDEX +- [x] LSET +- [x] LINSERT +- [x] LREM +- [x] LTRIM +- [x] RPOPLPUSH +- [x] LPOS +- [x] LMOVE +- [x] BLPOP +- [x] BRPOP +- [x] BLMOVE +- [x] BRPOPLPUSH +- [x] LMPOP +- [x] BLMPOP + +## Set Operations (9 commands) + +- [x] SINTER +- [x] SINTERSTORE +- [x] SINTERCARD +- [x] SUNION +- [x] SUNIONSTORE +- [x] SDIFF +- [x] SDIFFSTORE +- [x] SMISMEMBER +- [x] SSCAN + +## Sorted Set Operations (28 commands) + +### Tier 1 - Simple (6 commands) +- [x] ZINCRBY +- [x] ZCOUNT +- [x] ZLEXCOUNT +- [x] ZREMRANGEBYRANK +- [x] ZREMRANGEBYSCORE +- [x] ZREMRANGEBYLEX + +### Tier 2 - Simple Variadic (2 commands) +- [x] ZREM +- [x] ZMSCORE + +### Tier 3 - Complex Options (9 commands) +- [x] ZADD +- [x] ZRANGE +- [x] ZREVRANGE +- [x] ZRANGEBYSCORE +- [x] ZREVRANGEBYSCORE +- [x] ZRANGEBYLEX +- [x] ZREVRANGEBYLEX +- [x] ZRANGESTORE +- [x] ZSCAN + +### Tier 4 - Set Operations (7 commands) +- [x] ZINTER +- [x] ZINTERSTORE +- [x] ZINTERCARD +- [x] ZDIFF +- [x] ZDIFFSTORE +- [x] ZUNION +- [x] ZUNIONSTORE + +### Tier 5 - Blocking (4 commands) +- [x] BZPOPMIN +- [x] BZPOPMAX +- [x] ZMPOP +- [x] BZMPOP + +## Hash Operations (18 commands) + +### Tier 1 - Ergonomic Set Operations (2 commands) +- [ ] HSET (supports Map/Record OR variadic field/value pairs) +- [ ] HSETNX + +### Tier 2 - Basic Operations (4 commands) +- [ ] HDEL +- [ ] HEXISTS +- [ ] HRANDFIELD +- [ ] HSCAN + +### Tier 3 - Get/Set with Options (3 commands) +- [ ] HGETDEL +- [ ] HGETEX +- [ ] HSETEX + +### Tier 4 - Field Expiration (9 commands) +- [ ] HEXPIRE +- [ ] HEXPIREAT +- [ ] HEXPIRETIME +- [ ] HPERSIST +- [ ] HPEXPIRE +- [ ] HPEXPIREAT +- [ ] HPEXPIRETIME +- [ ] HPTTL +- [ ] HTTL + +## Key Management (8 commands) + +- [x] RENAME +- [x] RENAMENX +- [x] COPY +- [x] UNLINK +- [x] TYPE +- [x] TOUCH +- [x] RANDOMKEY +- [x] SCAN + +## Key Expiration & TTL (3 commands) + +- [x] EXPIREAT +- [x] PEXPIRE +- [x] PEXPIREAT + +--- + +**Total: 92 commands** \ No newline at end of file From f3bd6e6702acd30218d8bd6b1da435decac8c036 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 23:00:30 -0700 Subject: [PATCH 16/45] hset/hmset types --- packages/bun-types/redis.d.ts | 89 ++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 7 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 01b7c9388a0f..765ec5153da8 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -309,13 +309,68 @@ declare module "bun" { */ ttl(key: RedisClient.KeyLike): Promise; + /** + * Set the value of a hash field or multiple fields + * @param key The hash key + * @param fields Object/Record with field-value pairs + * @returns Promise that resolves with the number of fields that were added + */ + hset(key: RedisClient.KeyLike, fields: Record): Promise; + + /** + * Set the value of a hash field or multiple fields (variadic) + * @param key The hash key + * @param field The field name + * @param value The value to set + * @param rest Additional field-value pairs + * @returns Promise that resolves with the number of fields that were added + */ + hset( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + value: RedisClient.KeyLike, + ...rest: RedisClient.KeyLike[] + ): Promise; + /** * Set multiple hash fields to multiple values + * + * @deprecated Use {@link hset} instead. Since Redis 4.0.0, `HSET` supports multiple field-value pairs. + * + * @param key The hash key + * @param fields Object/Record with field-value pairs + * @returns Promise that resolves with "OK" + */ + hmset(key: RedisClient.KeyLike, fields: Record): Promise<"OK">; + + /** + * Set multiple hash fields to multiple values (variadic) + * + * @deprecated Use {@link hset} instead. Since Redis 4.0.0, `HSET` supports multiple field-value pairs. + * + * @param key The hash key + * @param field The field name + * @param value The value to set + * @param rest Additional field-value pairs + * @returns Promise that resolves with "OK" + */ + hmset( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + value: RedisClient.KeyLike, + ...rest: RedisClient.KeyLike[] + ): Promise<"OK">; + + /** + * Set multiple hash fields to multiple values (array syntax, backward compat) + * + * @deprecated Use {@link hset} instead. Since Redis 4.0.0, `HSET` supports multiple field-value pairs. + * * @param key The hash key * @param fieldValues An array of alternating field names and values - * @returns Promise that resolves with "OK" on success + * @returns Promise that resolves with "OK" */ - hmset(key: RedisClient.KeyLike, fieldValues: string[]): Promise; + hmset(key: RedisClient.KeyLike, fieldValues: RedisClient.KeyLike[]): Promise<"OK">; /** * Get the value of a hash field @@ -390,7 +445,11 @@ declare module "bun" { * @param keys Additional set keys to subtract from the first set * @returns Promise that resolves with the number of elements in the resulting set */ - sdiffstore(destination: RedisClient.KeyLike, key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + sdiffstore( + destination: RedisClient.KeyLike, + key: RedisClient.KeyLike, + ...keys: RedisClient.KeyLike[] + ): Promise; /** * Check if multiple members are members of a set @@ -399,7 +458,11 @@ declare module "bun" { * @param members Additional members to check * @returns Promise that resolves with an array of 1s and 0s indicating membership */ - smismember(key: RedisClient.KeyLike, member: RedisClient.KeyLike, ...members: RedisClient.KeyLike[]): Promise; + smismember( + key: RedisClient.KeyLike, + member: RedisClient.KeyLike, + ...members: RedisClient.KeyLike[] + ): Promise; /** * Incrementally iterate over a set @@ -956,7 +1019,11 @@ declare module "bun" { * @param keys Additional set keys to intersect * @returns Promise that resolves with the number of elements in the resulting set */ - sinterstore(destination: RedisClient.KeyLike, key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + sinterstore( + destination: RedisClient.KeyLike, + key: RedisClient.KeyLike, + ...keys: RedisClient.KeyLike[] + ): Promise; /** * Get the cardinality of the intersection of multiple sets @@ -965,7 +1032,11 @@ declare module "bun" { * @param args Additional set keys and optional LIMIT argument * @returns Promise that resolves with the number of elements in the intersection */ - sintercard(numkeys: number, key: RedisClient.KeyLike, ...args: (RedisClient.KeyLike | "LIMIT" | number)[]): Promise; + sintercard( + numkeys: number, + key: RedisClient.KeyLike, + ...args: (RedisClient.KeyLike | "LIMIT" | number)[] + ): Promise; /** * Get the length of the value stored in a key @@ -990,7 +1061,11 @@ declare module "bun" { * @param keys Additional set keys to union * @returns Promise that resolves with the number of elements in the resulting set */ - sunionstore(destination: RedisClient.KeyLike, key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + sunionstore( + destination: RedisClient.KeyLike, + key: RedisClient.KeyLike, + ...keys: RedisClient.KeyLike[] + ): Promise; /** * Determine the type of value stored at key From 859e94dbf04e449a3c553294fd5e23c90cc8e618 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 29 Sep 2025 23:09:39 -0700 Subject: [PATCH 17/45] hset/hmset tests --- src/bun.js/api/valkey.classes.ts | 6 +- src/valkey/js_valkey.zig | 1 + test/js/valkey/valkey.test.ts | 152 +++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 1 deletion(-) diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index f3c2f5ba10f2..ae7243e77983 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -98,7 +98,11 @@ export default [ }, hmset: { fn: "hmset", - length: 3, + length: 2, + }, + hset: { + fn: "hset", + length: 2, }, hget: { fn: "hget", diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 52ca7d540c7b..9187b0977e2c 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1270,6 +1270,7 @@ pub const JSValkeyClient = struct { pub const hlen = fns.hlen; pub const hmget = fns.hmget; pub const hmset = fns.hmset; + pub const hset = fns.hset; pub const hstrlen = fns.hstrlen; pub const hvals = fns.hvals; pub const incr = fns.incr; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index d820ceb3d0f0..ed77535d21f2 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -3976,6 +3976,158 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); }); + describe("Hash Operations", () => { + test("should set hash fields using object syntax", async () => { + const redis = ctx.redis; + const key = "hash-object-test"; + + const result = await redis.hset(key, { field1: "value1", field2: "value2", field3: "value3" }); + expect(result).toBe(3); // 3 new fields added + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("value1"); + const value2 = await redis.hget(key, "field2"); + expect(value2).toBe("value2"); + const value3 = await redis.hget(key, "field3"); + expect(value3).toBe("value3"); + }); + + test("should set hash fields using variadic syntax", async () => { + const redis = ctx.redis; + const key = "hash-variadic-test"; + + const result = await redis.hset(key, "field1", "value1", "field2", "value2"); + expect(result).toBe(2); // 2 new fields added + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("value1"); + const value2 = await redis.hget(key, "field2"); + expect(value2).toBe("value2"); + }); + + test("should set single hash field", async () => { + const redis = ctx.redis; + const key = "hash-single-test"; + + const result = await redis.hset(key, "field1", "value1"); + expect(result).toBe(1); // 1 new field added + + const value = await redis.hget(key, "field1"); + expect(value).toBe("value1"); + }); + + test("should update existing hash fields", async () => { + const redis = ctx.redis; + const key = "hash-update-test"; + + // Initial set + const result1 = await redis.hset(key, { field1: "value1", field2: "value2" }); + expect(result1).toBe(2); + + // Update existing and add new + const result2 = await redis.hset(key, { field1: "new-value1", field3: "value3" }); + expect(result2).toBe(1); // Only field3 is new + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("new-value1"); + const value3 = await redis.hget(key, "field3"); + expect(value3).toBe("value3"); + }); + + test("should work with HMSET using object syntax", async () => { + const redis = ctx.redis; + const key = "hmset-object-test"; + + const result = await redis.hmset(key, { field1: "value1", field2: "value2" }); + expect(result).toBe("OK"); + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("value1"); + const value2 = await redis.hget(key, "field2"); + expect(value2).toBe("value2"); + }); + + test("should work with HMSET using variadic syntax", async () => { + const redis = ctx.redis; + const key = "hmset-variadic-test"; + + const result = await redis.hmset(key, "field1", "value1", "field2", "value2"); + expect(result).toBe("OK"); + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("value1"); + }); + + test("should work with HMSET using array syntax", async () => { + const redis = ctx.redis; + const key = "hmset-array-test"; + + const result = await redis.hmset(key, ["field1", "value1", "field2", "value2"]); + expect(result).toBe("OK"); + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("value1"); + }); + + test("should handle numeric field names and values", async () => { + const redis = ctx.redis; + const key = "hash-numeric-test"; + + const result = await redis.hset(key, { 123: "value1", field2: 456 }); + expect(result).toBe(2); + + const value1 = await redis.hget(key, "123"); + expect(value1).toBe("value1"); + const value2 = await redis.hget(key, "field2"); + expect(value2).toBe("456"); + }); + + test("should throw error for odd number of variadic arguments", async () => { + const redis = ctx.redis; + const key = "hash-error-test"; + + expect(async () => { + await redis.hset(key, "field1", "value1", "field2"); + }).toThrow(); + }); + + test("should throw error for empty object", async () => { + const redis = ctx.redis; + const key = "hash-empty-test"; + + expect(async () => { + await redis.hset(key, {}); + }).toThrow(); + }); + + test("should throw error for array with odd number of elements", async () => { + const redis = ctx.redis; + const key = "hmset-error-test"; + + expect(async () => { + await redis.hmset(key, ["field1", "value1", "field2"]); + }).toThrow(); + }); + + test("should handle large number of fields", async () => { + const redis = ctx.redis; + const key = "hash-large-test"; + + const fields: Record = {}; + for (let i = 0; i < 100; i++) { + fields[`field${i}`] = `value${i}`; + } + + const result = await redis.hset(key, fields); + expect(result).toBe(100); + + const value0 = await redis.hget(key, "field0"); + expect(value0).toBe("value0"); + const value99 = await redis.hget(key, "field99"); + expect(value99).toBe("value99"); + }); + }); + describe("Connection State", () => { test("should have a connected property", () => { const redis = ctx.redis; From 8ceffca6d0e85bb0d4bfb70e2422ba951c475d34 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 00:08:28 -0700 Subject: [PATCH 18/45] hmset/hset impl --- packages/bun-types/redis.d.ts | 4 +- src/valkey/js_valkey_functions.zig | 120 ++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 39 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 765ec5153da8..559f4f784b44 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -315,7 +315,7 @@ declare module "bun" { * @param fields Object/Record with field-value pairs * @returns Promise that resolves with the number of fields that were added */ - hset(key: RedisClient.KeyLike, fields: Record): Promise; + hset(key: RedisClient.KeyLike, fields: Record): Promise; /** * Set the value of a hash field or multiple fields (variadic) @@ -341,7 +341,7 @@ declare module "bun" { * @param fields Object/Record with field-value pairs * @returns Promise that resolves with "OK" */ - hmset(key: RedisClient.KeyLike, fields: Record): Promise<"OK">; + hmset(key: RedisClient.KeyLike, fields: Record): Promise<"OK">; /** * Set multiple hash fields to multiple values (variadic) diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 3f6c9c7e39ab..baff6cb53fd1 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -534,70 +534,116 @@ pub fn hincrbyfloat(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, ca return promise.toJS(); } -// Implement hmset (set multiple values in hash) -pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { - try requireNotSubscriber(this, @src().fn_name); +fn hsetImpl(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, comptime command: []const u8) bun.JSError!JSValue { + try requireNotSubscriber(this, command); 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", .{}); - } - - 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", .{}); - } + const second_arg = callframe.argument(1); - 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(); - } + for (args.items) |item| item.deinit(); args.deinit(); } - // 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.toUTF8(bun.default_allocator)); - // 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); + if (second_arg.isObject() and !second_arg.isArray()) { + // Pattern 1: Object/Record - hset(key, {field: value, ...}) + const obj = second_arg.getObject() orelse { + return globalObject.throwInvalidArgumentType(command, "fields", "object"); + }; - // Add value - if (try iter.next()) |value_js| { - const value_str = try value_js.toBunString(globalObject); + var object_iter = try jsc.JSPropertyIterator(.{ + .skip_empty_name = false, + .include_value = true, + }).init(globalObject, obj); + defer object_iter.deinit(); + + try args.ensureTotalCapacity(1 + object_iter.len * 2); + + while (try object_iter.next()) |field_name| { + const field_slice = field_name.toUTF8(bun.default_allocator); + args.appendAssumeCapacity(field_slice); + + const value_str = try object_iter.value.toBunString(globalObject); defer value_str.deref(); - const value_slice = value_str.toUTF8WithoutRef(bun.default_allocator); + + const value_slice = value_str.toUTF8(bun.default_allocator); args.appendAssumeCapacity(value_slice); - } else { - return globalObject.throw("Arguments must be an array of alternating field names and values", .{}); + } + } else if (second_arg.isArray()) { + // Pattern 3: Array - hmset(key, [field, value, ...]) + var iter = try second_arg.arrayIterator(globalObject); + if (iter.len % 2 != 0) { + return globalObject.throw("Array must have an even number of elements (field-value pairs)", .{}); + } + + try args.ensureTotalCapacity(1 + iter.len); + + while (try iter.next()) |field_js| { + const field_str = try field_js.toBunString(globalObject); + args.appendAssumeCapacity(field_str.toUTF8(bun.default_allocator)); + field_str.deref(); + + const value_js = try iter.next() orelse { + return globalObject.throw("Array must have an even number of elements (field-value pairs)", .{}); + }; + const value_str = try value_js.toBunString(globalObject); + args.appendAssumeCapacity(value_str.toUTF8(bun.default_allocator)); + value_str.deref(); + } + } else { + // Pattern 2: Variadic - hset(key, field, value, ...) + const args_count = callframe.argumentsCount(); + if (args_count < 3) { + return globalObject.throw("HSET requires at least key, field, and value arguments", .{}); + } + + const field_value_count = args_count - 1; // Exclude key + if (field_value_count % 2 != 0) { + return globalObject.throw("HSET requires field-value pairs (even number of arguments after key)", .{}); + } + + try args.ensureTotalCapacity(args_count); + + var i: u32 = 1; + while (i < args_count) : (i += 1) { + const arg_str = try callframe.argument(i).toBunString(globalObject); + args.appendAssumeCapacity(arg_str.toUTF8(bun.default_allocator)); + arg_str.deref(); } } - // Send HMSET command + if (args.items.len == 1) { + return globalObject.throw("HSET requires at least one field-value pair", .{}); + } + const promise = this.send( globalObject, callframe.this(), &.{ - .command = "HMSET", + .command = command, .args = .{ .slices = args.items }, }, ) catch |err| { - return protocol.valkeyErrorToJS(globalObject, "Failed to send HMSET command", err); + const msg = if (bun.strings.eqlComptime(command, "HSET")) "Failed to send HSET command" else "Failed to send HMSET command"; + return protocol.valkeyErrorToJS(globalObject, msg, err); }; + return promise.toJS(); } +pub fn hset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + return hsetImpl(this, globalObject, callframe, "HSET"); +} + +pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + return hsetImpl(this, globalObject, callframe, "HMSET"); +} + // 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; From 68f96de9c0dd51d6b039dd53073a02912a774d80 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 00:11:16 -0700 Subject: [PATCH 19/45] types for hdel, hexists, hrandfield, hscan --- packages/bun-types/redis.d.ts | 98 +++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 559f4f784b44..ce4b3489b0de 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -388,6 +388,104 @@ declare module "bun" { */ hmget(key: RedisClient.KeyLike, fields: string[]): Promise>; + /** + * Delete one or more hash fields + * @param key The hash key + * @param field The field to delete + * @param rest Additional fields to delete + * @returns Promise that resolves with the number of fields that were removed + */ + hdel(key: RedisClient.KeyLike, field: RedisClient.KeyLike, ...rest: RedisClient.KeyLike[]): Promise; + + /** + * Determine if a hash field exists + * @param key The hash key + * @param field The field to check + * @returns Promise that resolves with true if the field exists, false otherwise + */ + hexists(key: RedisClient.KeyLike, field: RedisClient.KeyLike): Promise; + + /** + * Get one or multiple random fields from a hash + * @param key The hash key + * @returns Promise that resolves with a random field name, or null if the hash doesn't exist + */ + hrandfield(key: RedisClient.KeyLike): Promise; + + /** + * Get one or multiple random fields from a hash + * @param key The hash key + * @param count The number of fields to return (positive for unique fields, negative for potentially duplicate fields) + * @returns Promise that resolves with an array of random field names + */ + hrandfield(key: RedisClient.KeyLike, count: number): Promise; + + /** + * Get one or multiple random fields with values from a hash + * @param key The hash key + * @param count The number of fields to return + * @param withValues Literal "WITHVALUES" to include values + * @returns Promise that resolves with an array of alternating field names and values + */ + hrandfield(key: RedisClient.KeyLike, count: number, withValues: "WITHVALUES"): Promise; + + /** + * Incrementally iterate hash fields and values + * @param key The hash key + * @param cursor The cursor value (0 to start iteration) + * @returns Promise that resolves with [next_cursor, [field1, value1, field2, value2, ...]] + */ + hscan(key: RedisClient.KeyLike, cursor: number | string): Promise<[string, string[]]>; + + /** + * Incrementally iterate hash fields and values with pattern matching + * @param key The hash key + * @param cursor The cursor value (0 to start iteration) + * @param match Literal "MATCH" + * @param pattern Pattern to match field names against + * @returns Promise that resolves with [next_cursor, [field1, value1, field2, value2, ...]] + */ + hscan( + key: RedisClient.KeyLike, + cursor: number | string, + match: "MATCH", + pattern: string, + ): Promise<[string, string[]]>; + + /** + * Incrementally iterate hash fields and values with count limit + * @param key The hash key + * @param cursor The cursor value (0 to start iteration) + * @param count Literal "COUNT" + * @param limit Maximum number of fields to return per call + * @returns Promise that resolves with [next_cursor, [field1, value1, field2, value2, ...]] + */ + hscan( + key: RedisClient.KeyLike, + cursor: number | string, + count: "COUNT", + limit: number, + ): Promise<[string, string[]]>; + + /** + * Incrementally iterate hash fields and values with pattern and count + * @param key The hash key + * @param cursor The cursor value (0 to start iteration) + * @param match Literal "MATCH" + * @param pattern Pattern to match field names against + * @param count Literal "COUNT" + * @param limit Maximum number of fields to return per call + * @returns Promise that resolves with [next_cursor, [field1, value1, field2, value2, ...]] + */ + hscan( + key: RedisClient.KeyLike, + cursor: number | string, + match: "MATCH", + pattern: string, + count: "COUNT", + limit: number, + ): Promise<[string, string[]]>; + /** * Check if a value is a member of a set * @param key The set key From eee5b3ed73729192712f88ca8170ea54baa0af26 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 00:18:39 -0700 Subject: [PATCH 20/45] feat: impl hdel,hexists,hrandfield,hscan --- src/bun.js/api/valkey.classes.ts | 16 +++ src/valkey/js_valkey.zig | 4 + src/valkey/js_valkey_functions.zig | 29 ++++++ test/js/valkey/valkey.test.ts | 151 ++++++++++++++++++++++++++++- 4 files changed, 197 insertions(+), 3 deletions(-) diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index ae7243e77983..5936ef8728c9 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -112,6 +112,22 @@ export default [ fn: "hmget", length: 2, }, + hdel: { + fn: "hdel", + length: 2, + }, + hexists: { + fn: "hexists", + length: 2, + }, + hrandfield: { + fn: "hrandfield", + length: 1, + }, + hscan: { + fn: "hscan", + length: 2, + }, sismember: { fn: "sismember", length: 2, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 9187b0977e2c..985c496e267d 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1267,9 +1267,13 @@ pub const JSValkeyClient = struct { pub const hincrby = fns.hincrby; pub const hincrbyfloat = fns.hincrbyfloat; pub const hkeys = fns.hkeys; + pub const hdel = fns.hdel; + pub const hexists = fns.hexists; pub const hlen = fns.hlen; pub const hmget = fns.hmget; pub const hmset = fns.hmset; + pub const hrandfield = fns.hrandfield; + pub const hscan = fns.hscan; pub const hset = fns.hset; pub const hstrlen = fns.hstrlen; pub const hvals = fns.hvals; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index baff6cb53fd1..aba75bfca707 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -644,6 +644,35 @@ pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe return hsetImpl(this, globalObject, callframe, "HMSET"); } +pub const hdel = compile.@"(key: RedisKey, ...args: RedisKey[])"("hdel", "HDEL", "key", .not_subscriber).call; +pub const hrandfield = compile.@"(key: RedisKey, ...args: RedisKey[])"("hrandfield", "HRANDFIELD", "key", .not_subscriber).call; +pub const hscan = compile.@"(key: RedisKey, ...args: RedisKey[])"("hscan", "HSCAN", "key", .not_subscriber).call; + +pub fn hexists(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + try requireNotSubscriber(this, "hexists"); + + const key = (try fromJS(globalObject, callframe.argument(0))) orelse + return globalObject.throwInvalidArgumentType("hexists", "key", "string or buffer"); + defer key.deinit(); + + const field = (try fromJS(globalObject, callframe.argument(1))) orelse + return globalObject.throwInvalidArgumentType("hexists", "field", "string or buffer"); + defer field.deinit(); + + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = "HEXISTS", + .args = .{ .args = &.{ key, field } }, + .meta = .{ .return_as_bool = true }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send HEXISTS 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; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index ed77535d21f2..f96f14bbd502 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -4088,7 +4088,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(async () => { await redis.hset(key, "field1", "value1", "field2"); - }).toThrow(); + }).toThrow("HSET requires field-value pairs (even number of arguments after key)"); }); test("should throw error for empty object", async () => { @@ -4097,7 +4097,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(async () => { await redis.hset(key, {}); - }).toThrow(); + }).toThrow("HSET requires at least one field-value pair"); }); test("should throw error for array with odd number of elements", async () => { @@ -4106,7 +4106,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(async () => { await redis.hmset(key, ["field1", "value1", "field2"]); - }).toThrow(); + }).toThrow("Array must have an even number of elements (field-value pairs)"); }); test("should handle large number of fields", async () => { @@ -4126,6 +4126,151 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const value99 = await redis.hget(key, "field99"); expect(value99).toBe("value99"); }); + + test("should delete hash fields using hdel", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const deleted = await redis.hdel(key, "age"); + expect(deleted).toBe(1); + + const age = await redis.hget(key, "age"); + expect(age).toBeNull(); + + const name = await redis.hget(key, "name"); + expect(name).toBe("John"); + }); + + test("should delete multiple hash fields using hdel", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC", country: "USA" }); + + const deleted = await redis.hdel(key, "age", "city"); + expect(deleted).toBe(2); + + const remaining = await redis.hgetall(key); + expect(remaining).toEqual({ name: "John", country: "USA" }); + }); + + test("should check if hash field exists using hexists", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const nameExists = await redis.hexists(key, "name"); + expect(nameExists).toBe(true); + + const emailExists = await redis.hexists(key, "email"); + expect(emailExists).toBe(false); + }); + + test("should get random field using hrandfield", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const field = await redis.hrandfield(key); + expect(["name", "age", "city"]).toContain(field); + }); + + test("should get multiple random fields using hrandfield with count", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const fields = await redis.hrandfield(key, 2); + expect(fields).toBeInstanceOf(Array); + expect(fields.length).toBe(2); + fields.forEach(field => { + expect(["name", "age", "city"]).toContain(field); + }); + }); + + test("should get random fields with values using hrandfield WITHVALUES", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const result = await redis.hrandfield(key, 1, "WITHVALUES"); + expect(result).toBeInstanceOf(Array); + expect(result.length).toBe(2); // [field, value] + + const fieldName = result[0]; + const fieldValue = result[1]; + expect(["name", "age"]).toContain(fieldName); + if (fieldName === "name") { + expect(fieldValue).toBe("John"); + } else { + expect(fieldValue).toBe("30"); + } + }); + + test("should scan hash using hscan", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const [cursor, fields] = await redis.hscan(key, 0); + expect(typeof cursor).toBe("string"); + expect(fields).toBeInstanceOf(Array); + expect(fields.length).toBe(6); // [field1, value1, field2, value2, field3, value3] + + // Convert to object for easier testing + const obj: Record = {}; + for (let i = 0; i < fields.length; i += 2) { + obj[fields[i]] = fields[i + 1]; + } + expect(obj).toEqual({ name: "John", age: "30", city: "NYC" }); + }); + + test("should scan hash with pattern using hscan MATCH", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { field1: "val1", field2: "val2", other: "val3" }); + + const [cursor, fields] = await redis.hscan(key, 0, "MATCH", "field*"); + expect(typeof cursor).toBe("string"); + expect(fields).toBeInstanceOf(Array); + + // Convert to object + const obj: Record = {}; + for (let i = 0; i < fields.length; i += 2) { + obj[fields[i]] = fields[i + 1]; + } + + // Should only contain fields matching "field*" + expect(obj.field1).toBe("val1"); + expect(obj.field2).toBe("val2"); + expect(obj.other).toBeUndefined(); + }); + + test("should scan hash with count using hscan COUNT", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + // Add many fields + const fields: Record = {}; + for (let i = 0; i < 20; i++) { + fields[`field${i}`] = `value${i}`; + } + await redis.hset(key, fields); + + const [cursor, result] = await redis.hscan(key, 0, "COUNT", 5); + expect(typeof cursor).toBe("string"); + expect(result).toBeInstanceOf(Array); + // COUNT is a hint, so we just check we got some results + expect(result.length).toBeGreaterThan(0); + }); }); describe("Connection State", () => { From b17268ed62ac2e0ede42f80fc350f3d9ab940da1 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 00:25:37 -0700 Subject: [PATCH 21/45] fix hrandfield failing test --- packages/bun-types/redis.d.ts | 2 +- test/js/valkey/valkey.test.ts | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index ce4b3489b0de..8fe70dce37d9 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -427,7 +427,7 @@ declare module "bun" { * @param withValues Literal "WITHVALUES" to include values * @returns Promise that resolves with an array of alternating field names and values */ - hrandfield(key: RedisClient.KeyLike, count: number, withValues: "WITHVALUES"): Promise; + hrandfield(key: RedisClient.KeyLike, count: number, withValues: "WITHVALUES"): Promise<[string, string][]>; /** * Incrementally iterate hash fields and values diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index f96f14bbd502..b8b3f95fbf4f 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -980,24 +980,22 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should handle LSET errors", async () => { const redis = ctx.redis; - // Test out of range index on existing list await redis.lpush("lset-error-test", "value"); - // Out of range should throw an error expect(async () => { await redis.lset("lset-error-test", 10, "newvalue"); - }).toThrow(); + }).toThrow(/index out of range/i); // Non-existent key should throw an error expect(async () => { await redis.lset("nonexistent-list", 0, "value"); - }).toThrow(); + }).toThrow(/no such key/i); // Wrong type (not a list) should throw an error await redis.set("string-key", "value"); expect(async () => { await redis.lset("string-key", 0, "value"); - }).toThrow(); + }).toThrow(/wrong.*type|WRONGTYPE/i); }); test("should handle LRANGE with various ranges", async () => { @@ -4197,19 +4195,19 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); - await redis.hset(key, { name: "John", age: "30" }); + const fullData = { name: "Andy", age: "30", city: "Cupertino" }; + await redis.hset(key, fullData); - const result = await redis.hrandfield(key, 1, "WITHVALUES"); + const result = await redis.hrandfield(key, 2, "WITHVALUES"); expect(result).toBeInstanceOf(Array); - expect(result.length).toBe(2); // [field, value] - - const fieldName = result[0]; - const fieldValue = result[1]; - expect(["name", "age"]).toContain(fieldName); - if (fieldName === "name") { - expect(fieldValue).toBe("John"); - } else { - expect(fieldValue).toBe("30"); + expect(result.length).toBe(2); + + const obj = Object.fromEntries(result); + + expect(Object.keys(obj).length).toBe(2); + + for (const [field, value] of Object.entries(obj)) { + expect(fullData).toHaveProperty(field, value); } }); @@ -4667,7 +4665,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await subscriber.subscribe(channel, () => {}); // Publishing from the same client should work - expect(async () => subscriber.publish(channel, "self-published")).toThrow(); + expect(async () => subscriber.publish(channel, "self-published")).toThrow( + "RedisClient.prototype.publish cannot be called while in subscriber mode.", + ); }); test("complete unsubscribe restores normal command mode", async () => { From 1ccee32de36b3343f66dbd31859d2d9c796a89bf Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 00:31:01 -0700 Subject: [PATCH 22/45] feat: impl hsetnx --- packages/bun-types/redis.d.ts | 9 +++++++++ src/bun.js/api/valkey.classes.ts | 4 ++++ src/valkey/js_valkey.zig | 1 + src/valkey/js_valkey_functions.zig | 30 ++++++++++++++++++++++++++++++ test/js/valkey/valkey.test.ts | 17 +++++++++++++++++ 5 files changed, 61 insertions(+) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 8fe70dce37d9..6141ebe5b222 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -332,6 +332,15 @@ declare module "bun" { ...rest: RedisClient.KeyLike[] ): Promise; + /** + * Set the value of a hash field, only if the field does not exist + * @param key The hash key + * @param field The field to set + * @param value The value to set + * @returns Promise that resolves with true if field was set, false if field already exists + */ + hsetnx(key: RedisClient.KeyLike, field: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + /** * Set multiple hash fields to multiple values * diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 5936ef8728c9..927dd6dbab07 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -104,6 +104,10 @@ export default [ fn: "hset", length: 2, }, + hsetnx: { + fn: "hsetnx", + length: 3, + }, hget: { fn: "hget", length: 2, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 985c496e267d..a6929a84480c 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1275,6 +1275,7 @@ pub const JSValkeyClient = struct { pub const hrandfield = fns.hrandfield; pub const hscan = fns.hscan; pub const hset = fns.hset; + pub const hsetnx = fns.hsetnx; pub const hstrlen = fns.hstrlen; pub const hvals = fns.hvals; pub const incr = fns.incr; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index aba75bfca707..5e5ff4f51082 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -648,6 +648,36 @@ pub const hdel = compile.@"(key: RedisKey, ...args: RedisKey[])"("hdel", "HDEL", pub const hrandfield = compile.@"(key: RedisKey, ...args: RedisKey[])"("hrandfield", "HRANDFIELD", "key", .not_subscriber).call; pub const hscan = compile.@"(key: RedisKey, ...args: RedisKey[])"("hscan", "HSCAN", "key", .not_subscriber).call; +pub fn hsetnx(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + try requireNotSubscriber(this, "hsetnx"); + + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { + return globalObject.throwInvalidArgumentType("hsetnx", "key", "string or buffer"); + }; + defer key.deinit(); + const field = (try fromJS(globalObject, callframe.argument(1))) orelse { + return globalObject.throwInvalidArgumentType("hsetnx", "field", "string or buffer"); + }; + defer field.deinit(); + const value = (try fromJS(globalObject, callframe.argument(2))) orelse { + return globalObject.throwInvalidArgumentType("hsetnx", "value", "string or buffer"); + }; + defer value.deinit(); + + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = "HSETNX", + .args = .{ .args = &.{ key, field, value } }, + .meta = .{ .return_as_bool = true }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send HSETNX command", err); + }; + return promise.toJS(); +} + pub fn hexists(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, "hexists"); diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index b8b3f95fbf4f..55e75ebe09f9 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -4125,6 +4125,23 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(value99).toBe("value99"); }); + test("should set hash field only if it doesn't exist using hsetnx", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result1 = await redis.hsetnx(key, "name", "John"); + expect(result1).toBe(true); + + const result2 = await redis.hsetnx(key, "name", "Jane"); + expect(result2).toBe(false); + + const value = await redis.hget(key, "name"); + expect(value).toBe("John"); + + const result3 = await redis.hsetnx(key, "age", "30"); + expect(result3).toBe(true); + }); + test("should delete hash fields using hdel", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); From 17e5861474656fcedcfdd0055883423046000011 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 00:35:48 -0700 Subject: [PATCH 23/45] hgetdel/hgetex/hsetex --- packages/bun-types/redis.d.ts | 131 +++++++++++++++++++++++++++++ src/bun.js/api/valkey.classes.ts | 12 +++ src/valkey/js_valkey.zig | 3 + src/valkey/js_valkey_functions.zig | 3 + test/js/valkey/valkey.test.ts | 48 +++++++++++ 5 files changed, 197 insertions(+) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 6141ebe5b222..0edd0f804411 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -341,6 +341,137 @@ declare module "bun" { */ hsetnx(key: RedisClient.KeyLike, field: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + /** + * Get the value of a hash field and delete the field + * @param key The hash key + * @param field The field to get and delete + * @returns Promise that resolves with the field value or null if the field doesn't exist + */ + hgetdel(key: RedisClient.KeyLike, field: RedisClient.KeyLike): Promise; + + /** + * Get the value of a hash field with expiration options + * @param key The hash key + * @param field The field to get + * @param ex Literal "EX" + * @param seconds Expiration time in seconds + * @returns Promise that resolves with the field value or null if the field doesn't exist + */ + hgetex(key: RedisClient.KeyLike, field: RedisClient.KeyLike, ex: "EX", seconds: number): Promise; + + /** + * Get the value of a hash field with expiration options + * @param key The hash key + * @param field The field to get + * @param exat Literal "EXAT" + * @param timestamp Unix timestamp for expiration + * @returns Promise that resolves with the field value or null if the field doesn't exist + */ + hgetex( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + exat: "EXAT", + timestamp: number, + ): Promise; + + /** + * Get the value of a hash field with expiration options + * @param key The hash key + * @param field The field to get + * @param px Literal "PX" + * @param milliseconds Expiration time in milliseconds + * @returns Promise that resolves with the field value or null if the field doesn't exist + */ + hgetex( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + px: "PX", + milliseconds: number, + ): Promise; + + /** + * Get the value of a hash field with expiration options + * @param key The hash key + * @param field The field to get + * @param pxat Literal "PXAT" + * @param timestamp Unix timestamp in milliseconds for expiration + * @returns Promise that resolves with the field value or null if the field doesn't exist + */ + hgetex( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + pxat: "PXAT", + timestamp: number, + ): Promise; + + /** + * Set the value of a hash field with expiration + * @param key The hash key + * @param field The field to set + * @param value The value to set + * @param ex Literal "EX" + * @param seconds Expiration time in seconds + * @returns Promise that resolves with "OK" + */ + hsetex( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + value: RedisClient.KeyLike, + ex: "EX", + seconds: number, + ): Promise<"OK">; + + /** + * Set the value of a hash field with expiration + * @param key The hash key + * @param field The field to set + * @param value The value to set + * @param exat Literal "EXAT" + * @param timestamp Unix timestamp for expiration + * @returns Promise that resolves with "OK" + */ + hsetex( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + value: RedisClient.KeyLike, + exat: "EXAT", + timestamp: number, + ): Promise<"OK">; + + /** + * Set the value of a hash field with expiration + * @param key The hash key + * @param field The field to set + * @param value The value to set + * @param px Literal "PX" + * @param milliseconds Expiration time in milliseconds + * @returns Promise that resolves with "OK" + */ + hsetex( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + value: RedisClient.KeyLike, + px: "PX", + milliseconds: number, + ): Promise<"OK">; + + /** + * Set the value of a hash field with expiration + * @param key The hash key + * @param field The field to set + * @param value The value to set + * @param pxat Literal "PXAT" + * @param timestamp Unix timestamp in milliseconds for expiration + * @returns Promise that resolves with "OK" + */ + hsetex( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + value: RedisClient.KeyLike, + pxat: "PXAT", + timestamp: number, + ): Promise<"OK">; + /** * Set multiple hash fields to multiple values * diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 927dd6dbab07..1107bb6b8cae 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -132,6 +132,18 @@ export default [ fn: "hscan", length: 2, }, + hgetdel: { + fn: "hgetdel", + length: 2, + }, + hgetex: { + fn: "hgetex", + length: 2, + }, + hsetex: { + fn: "hsetex", + length: 3, + }, sismember: { fn: "sismember", length: 2, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index a6929a84480c..eeae2ab8fd50 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1269,12 +1269,15 @@ pub const JSValkeyClient = struct { pub const hkeys = fns.hkeys; pub const hdel = fns.hdel; pub const hexists = fns.hexists; + pub const hgetdel = fns.hgetdel; + pub const hgetex = fns.hgetex; pub const hlen = fns.hlen; pub const hmget = fns.hmget; pub const hmset = fns.hmset; pub const hrandfield = fns.hrandfield; pub const hscan = fns.hscan; pub const hset = fns.hset; + pub const hsetex = fns.hsetex; pub const hsetnx = fns.hsetnx; pub const hstrlen = fns.hstrlen; pub const hvals = fns.hvals; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 5e5ff4f51082..ef8fc9d54dff 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -647,6 +647,9 @@ pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe pub const hdel = compile.@"(key: RedisKey, ...args: RedisKey[])"("hdel", "HDEL", "key", .not_subscriber).call; pub const hrandfield = compile.@"(key: RedisKey, ...args: RedisKey[])"("hrandfield", "HRANDFIELD", "key", .not_subscriber).call; pub const hscan = compile.@"(key: RedisKey, ...args: RedisKey[])"("hscan", "HSCAN", "key", .not_subscriber).call; +pub const hgetdel = compile.@"(key: RedisKey, value: RedisValue)"("hgetdel", "HGETDEL", "key", "field", .not_subscriber).call; +pub const hgetex = compile.@"(...strings: string[])"("hgetex", "HGETEX", .not_subscriber).call; +pub const hsetex = compile.@"(...strings: string[])"("hsetex", "HSETEX", .not_subscriber).call; pub fn hsetnx(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, "hsetnx"); diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 55e75ebe09f9..03d36c78bd5c 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -4142,6 +4142,54 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(result3).toBe(true); }); + test("should get and delete hash field using hgetdel", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const value = await redis.hgetdel(key, "name"); + expect(value).toBe("John"); + + const check = await redis.hget(key, "name"); + expect(check).toBeNull(); + + const age = await redis.hget(key, "age"); + expect(age).toBe("30"); + }); + + test("should get hash field with expiration using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const value = await redis.hgetex(key, "name", "EX", 10); + expect(value).toBe("John"); + + const check = await redis.hget(key, "name"); + expect(check).toBe("John"); + + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(10); + }); + + test("should set hash field with expiration using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hsetex(key, "name", "John", "EX", 10); + expect(result).toBe("OK"); + + const value = await redis.hget(key, "name"); + expect(value).toBe("John"); + + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(10); + }); + test("should delete hash fields using hdel", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); From 7e8b5f3dd69eaa1a41ba1e5564da64a09ef34e2f Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 00:41:22 -0700 Subject: [PATCH 24/45] test with redis 8 --- test/js/valkey/docker-unified/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/valkey/docker-unified/Dockerfile b/test/js/valkey/docker-unified/Dockerfile index 7b03eba6fa28..9af9112d6641 100644 --- a/test/js/valkey/docker-unified/Dockerfile +++ b/test/js/valkey/docker-unified/Dockerfile @@ -1,5 +1,5 @@ # Unified Dockerfile for Valkey/Redis with TCP, TLS, Unix socket, and authentication support -FROM redis:7-alpine +FROM redis:8-alpine # Set user to root USER root From 4cc00183b17093511f7dfc838a60a9cba078fabd Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 01:04:46 -0700 Subject: [PATCH 25/45] feat: Implement HGETDEL, HGETEX, HSETEX, HEXPIRE, HEXPIREAT, HEXPIRETIME, HPERSIST, HPEXPIRE, HPEXPIREAT, HPEXPIRETIME, HPTTL and HTTL --- packages/bun-types/redis.d.ts | 297 +++++++++++++++++++---------- src/bun.js/api/valkey.classes.ts | 36 ++++ src/valkey/js_valkey.zig | 9 + src/valkey/js_valkey_functions.zig | 11 +- test/docker/docker-compose.yml | 9 +- test/js/valkey/valkey.test.ts | 281 +++++++++++++++++++++++++-- 6 files changed, 530 insertions(+), 113 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 0edd0f804411..f527731982c7 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -342,135 +342,240 @@ declare module "bun" { hsetnx(key: RedisClient.KeyLike, field: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; /** - * Get the value of a hash field and delete the field + * Get and delete one or more hash fields (Redis 8.0.0+) + * Syntax: HGETDEL key FIELDS numfields field [field ...] * @param key The hash key - * @param field The field to get and delete - * @returns Promise that resolves with the field value or null if the field doesn't exist + * @param fieldsKeyword Must be the literal string "FIELDS" + * @param numfields Number of fields to follow + * @param fields The field names to get and delete + * @returns Promise that resolves with array of field values (null for non-existent fields) + * @example redis.hgetdel("mykey", "FIELDS", 2, "field1", "field2") */ - hgetdel(key: RedisClient.KeyLike, field: RedisClient.KeyLike): Promise; + hgetdel( + key: RedisClient.KeyLike, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise>; + + /** + * Get hash field values with expiration options (Redis 8.0.0+) + * Syntax: HGETEX key [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST] FIELDS numfields field [field ...] + * @example redis.hgetex("mykey", "FIELDS", 1, "field1") + * @example redis.hgetex("mykey", "EX", 10, "FIELDS", 1, "field1") + * @example redis.hgetex("mykey", "PX", 5000, "FIELDS", 2, "field1", "field2") + * @example redis.hgetex("mykey", "PERSIST", "FIELDS", 1, "field1") + */ + //prettier-ignore + hgetex(key: RedisClient.KeyLike, fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + //prettier-ignore + hgetex(key: RedisClient.KeyLike, ex: "EX", seconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + //prettier-ignore + hgetex(key: RedisClient.KeyLike, px: "PX", milliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + //prettier-ignore + hgetex(key: RedisClient.KeyLike, exat: "EXAT", unixTimeSeconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + //prettier-ignore + hgetex(key: RedisClient.KeyLike, pxat: "PXAT", unixTimeMilliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + //prettier-ignore + hgetex(key: RedisClient.KeyLike, persist: "PERSIST", fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + + /** + * Set hash fields with expiration options (Redis 8.0.0+) + * Syntax: HSETEX key [FNX | FXX] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL] FIELDS numfields field value [field value ...] + * @example redis.hsetex("mykey", "FIELDS", 1, "field1", "value1") + * @example redis.hsetex("mykey", "EX", 10, "FIELDS", 1, "field1", "value1") + * @example redis.hsetex("mykey", "FNX", "EX", 10, "FIELDS", 1, "field1", "value1") + */ + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, ex: "EX", seconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, px: "PX", milliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, exat: "EXAT", unixTimeSeconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, pxat: "PXAT", unixTimeMilliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, keepttl: "KEEPTTL", fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", ex: "EX", seconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", px: "PX", milliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", exat: "EXAT", unixTimeSeconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", pxat: "PXAT", unixTimeMilliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", keepttl: "KEEPTTL", fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", ex: "EX", seconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", px: "PX", milliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", exat: "EXAT", unixTimeSeconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", pxat: "PXAT", unixTimeMilliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", keepttl: "KEEPTTL", fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + + /** + * Set expiration for hash fields (Redis 7.4+) + * Syntax: HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), 0 (condition not met), 1 (expiration set), 2 (field deleted) + * @example redis.hexpire("mykey", 10, "FIELDS", 1, "field1") + * @example redis.hexpire("mykey", 10, "NX", "FIELDS", 2, "field1", "field2") + */ + hexpire( + key: RedisClient.KeyLike, + seconds: number, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + hexpire( + key: RedisClient.KeyLike, + seconds: number, + condition: "NX" | "XX" | "GT" | "LT", + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; /** - * Get the value of a hash field with expiration options - * @param key The hash key - * @param field The field to get - * @param ex Literal "EX" - * @param seconds Expiration time in seconds - * @returns Promise that resolves with the field value or null if the field doesn't exist + * Set expiration for hash fields using Unix timestamp in seconds (Redis 7.4+) + * Syntax: HEXPIREAT key unix-time-seconds [NX | XX | GT | LT] FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), 0 (condition not met), 1 (expiration set), 2 (field deleted) + * @example redis.hexpireat("mykey", 1735689600, "FIELDS", 1, "field1") */ - hgetex(key: RedisClient.KeyLike, field: RedisClient.KeyLike, ex: "EX", seconds: number): Promise; + hexpireat( + key: RedisClient.KeyLike, + unixTimeSeconds: number, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + hexpireat( + key: RedisClient.KeyLike, + unixTimeSeconds: number, + condition: "NX" | "XX" | "GT" | "LT", + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; /** - * Get the value of a hash field with expiration options - * @param key The hash key - * @param field The field to get - * @param exat Literal "EXAT" - * @param timestamp Unix timestamp for expiration - * @returns Promise that resolves with the field value or null if the field doesn't exist + * Get expiration time of hash fields as Unix timestamp in seconds (Redis 7.4+) + * Syntax: HEXPIRETIME key FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), -1 (no expiration), Unix timestamp in seconds + * @example redis.hexpiretime("mykey", "FIELDS", 2, "field1", "field2") */ - hgetex( + hexpiretime( key: RedisClient.KeyLike, - field: RedisClient.KeyLike, - exat: "EXAT", - timestamp: number, - ): Promise; + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; /** - * Get the value of a hash field with expiration options - * @param key The hash key - * @param field The field to get - * @param px Literal "PX" - * @param milliseconds Expiration time in milliseconds - * @returns Promise that resolves with the field value or null if the field doesn't exist + * Remove expiration from hash fields (Redis 7.4+) + * Syntax: HPERSIST key FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), -1 (no expiration), 1 (expiration removed) + * @example redis.hpersist("mykey", "FIELDS", 1, "field1") */ - hgetex( + hpersist( key: RedisClient.KeyLike, - field: RedisClient.KeyLike, - px: "PX", - milliseconds: number, - ): Promise; + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; /** - * Get the value of a hash field with expiration options - * @param key The hash key - * @param field The field to get - * @param pxat Literal "PXAT" - * @param timestamp Unix timestamp in milliseconds for expiration - * @returns Promise that resolves with the field value or null if the field doesn't exist + * Set expiration for hash fields in milliseconds (Redis 7.4+) + * Syntax: HPEXPIRE key milliseconds [NX | XX | GT | LT] FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), 0 (condition not met), 1 (expiration set), 2 (field deleted) + * @example redis.hpexpire("mykey", 10000, "FIELDS", 1, "field1") */ - hgetex( + hpexpire( key: RedisClient.KeyLike, - field: RedisClient.KeyLike, - pxat: "PXAT", - timestamp: number, - ): Promise; + milliseconds: number, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + hpexpire( + key: RedisClient.KeyLike, + milliseconds: number, + condition: "NX" | "XX" | "GT" | "LT", + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; /** - * Set the value of a hash field with expiration - * @param key The hash key - * @param field The field to set - * @param value The value to set - * @param ex Literal "EX" - * @param seconds Expiration time in seconds - * @returns Promise that resolves with "OK" + * Set expiration for hash fields using Unix timestamp in milliseconds (Redis 7.4+) + * Syntax: HPEXPIREAT key unix-time-milliseconds [NX | XX | GT | LT] FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), 0 (condition not met), 1 (expiration set), 2 (field deleted) + * @example redis.hpexpireat("mykey", 1735689600000, "FIELDS", 1, "field1") */ - hsetex( + hpexpireat( key: RedisClient.KeyLike, - field: RedisClient.KeyLike, - value: RedisClient.KeyLike, - ex: "EX", - seconds: number, - ): Promise<"OK">; + unixTimeMilliseconds: number, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + hpexpireat( + key: RedisClient.KeyLike, + unixTimeMilliseconds: number, + condition: "NX" | "XX" | "GT" | "LT", + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; /** - * Set the value of a hash field with expiration - * @param key The hash key - * @param field The field to set - * @param value The value to set - * @param exat Literal "EXAT" - * @param timestamp Unix timestamp for expiration - * @returns Promise that resolves with "OK" + * Get expiration time of hash fields as Unix timestamp in milliseconds (Redis 7.4+) + * Syntax: HPEXPIRETIME key FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), -1 (no expiration), Unix timestamp in milliseconds + * @example redis.hpexpiretime("mykey", "FIELDS", 2, "field1", "field2") */ - hsetex( + hpexpiretime( key: RedisClient.KeyLike, - field: RedisClient.KeyLike, - value: RedisClient.KeyLike, - exat: "EXAT", - timestamp: number, - ): Promise<"OK">; + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; /** - * Set the value of a hash field with expiration - * @param key The hash key - * @param field The field to set - * @param value The value to set - * @param px Literal "PX" - * @param milliseconds Expiration time in milliseconds - * @returns Promise that resolves with "OK" + * Get TTL of hash fields in milliseconds (Redis 7.4+) + * Syntax: HPTTL key FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), -1 (no expiration), TTL in milliseconds + * @example redis.hpttl("mykey", "FIELDS", 2, "field1", "field2") */ - hsetex( + hpttl( key: RedisClient.KeyLike, - field: RedisClient.KeyLike, - value: RedisClient.KeyLike, - px: "PX", - milliseconds: number, - ): Promise<"OK">; + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; /** - * Set the value of a hash field with expiration - * @param key The hash key - * @param field The field to set - * @param value The value to set - * @param pxat Literal "PXAT" - * @param timestamp Unix timestamp in milliseconds for expiration - * @returns Promise that resolves with "OK" + * Get TTL of hash fields in seconds (Redis 7.4+) + * Syntax: HTTL key FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), -1 (no expiration), TTL in seconds + * @example redis.httl("mykey", "FIELDS", 2, "field1", "field2") */ - hsetex( + httl( key: RedisClient.KeyLike, - field: RedisClient.KeyLike, - value: RedisClient.KeyLike, - pxat: "PXAT", - timestamp: number, - ): Promise<"OK">; + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; /** * Set multiple hash fields to multiple values diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 1107bb6b8cae..8ac41b2e32e3 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -144,6 +144,42 @@ export default [ fn: "hsetex", length: 3, }, + hexpire: { + fn: "hexpire", + length: 3, + }, + hexpireat: { + fn: "hexpireat", + length: 3, + }, + hexpiretime: { + fn: "hexpiretime", + length: 2, + }, + hpersist: { + fn: "hpersist", + length: 2, + }, + hpexpire: { + fn: "hpexpire", + length: 3, + }, + hpexpireat: { + fn: "hpexpireat", + length: 3, + }, + hpexpiretime: { + fn: "hpexpiretime", + length: 2, + }, + hpttl: { + fn: "hpttl", + length: 2, + }, + httl: { + fn: "httl", + length: 2, + }, sismember: { fn: "sismember", length: 2, diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index eeae2ab8fd50..3dd9d3b92aad 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1281,6 +1281,15 @@ pub const JSValkeyClient = struct { pub const hsetnx = fns.hsetnx; pub const hstrlen = fns.hstrlen; pub const hvals = fns.hvals; + pub const hexpire = fns.hexpire; + pub const hexpireat = fns.hexpireat; + pub const hexpiretime = fns.hexpiretime; + pub const hpersist = fns.hpersist; + pub const hpexpire = fns.hpexpire; + pub const hpexpireat = fns.hpexpireat; + pub const hpexpiretime = fns.hpexpiretime; + pub const hpttl = fns.hpttl; + pub const httl = fns.httl; pub const incr = fns.incr; pub const incrby = fns.incrby; pub const incrbyfloat = fns.incrbyfloat; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index ef8fc9d54dff..7181176c5041 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -647,9 +647,18 @@ pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe pub const hdel = compile.@"(key: RedisKey, ...args: RedisKey[])"("hdel", "HDEL", "key", .not_subscriber).call; pub const hrandfield = compile.@"(key: RedisKey, ...args: RedisKey[])"("hrandfield", "HRANDFIELD", "key", .not_subscriber).call; pub const hscan = compile.@"(key: RedisKey, ...args: RedisKey[])"("hscan", "HSCAN", "key", .not_subscriber).call; -pub const hgetdel = compile.@"(key: RedisKey, value: RedisValue)"("hgetdel", "HGETDEL", "key", "field", .not_subscriber).call; +pub const hgetdel = compile.@"(...strings: string[])"("hgetdel", "HGETDEL", .not_subscriber).call; pub const hgetex = compile.@"(...strings: string[])"("hgetex", "HGETEX", .not_subscriber).call; pub const hsetex = compile.@"(...strings: string[])"("hsetex", "HSETEX", .not_subscriber).call; +pub const hexpire = compile.@"(...strings: string[])"("hexpire", "HEXPIRE", .not_subscriber).call; +pub const hexpireat = compile.@"(...strings: string[])"("hexpireat", "HEXPIREAT", .not_subscriber).call; +pub const hexpiretime = compile.@"(...strings: string[])"("hexpiretime", "HEXPIRETIME", .not_subscriber).call; +pub const hpersist = compile.@"(...strings: string[])"("hpersist", "HPERSIST", .not_subscriber).call; +pub const hpexpire = compile.@"(...strings: string[])"("hpexpire", "HPEXPIRE", .not_subscriber).call; +pub const hpexpireat = compile.@"(...strings: string[])"("hpexpireat", "HPEXPIREAT", .not_subscriber).call; +pub const hpexpiretime = compile.@"(...strings: string[])"("hpexpiretime", "HPEXPIRETIME", .not_subscriber).call; +pub const hpttl = compile.@"(...strings: string[])"("hpttl", "HPTTL", .not_subscriber).call; +pub const httl = compile.@"(...strings: string[])"("httl", "HTTL", .not_subscriber).call; pub fn hsetnx(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, "hsetnx"); diff --git a/test/docker/docker-compose.yml b/test/docker/docker-compose.yml index 465ee5a147b8..c6618d48d6ad 100644 --- a/test/docker/docker-compose.yml +++ b/test/docker/docker-compose.yml @@ -153,6 +153,8 @@ services: context: ../js/valkey/docker-unified dockerfile: Dockerfile image: bun-redis-unified:local + environment: + SKIP_FIX_PERMS: "1" ports: - target: 6379 published: 0 @@ -163,8 +165,9 @@ services: protocol: tcp name: tls volumes: - - redis-unix:/tmp/redis - - redis-data:/data + - redis-unix:/tmp/redis:rw + tmpfs: + - /data # MinIO (S3) Service minio: @@ -208,8 +211,6 @@ services: volumes: redis-unix: - redis-data: - driver: local networks: default: diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 03d36c78bd5c..3b9dde99b902 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -4148,8 +4148,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, { name: "John", age: "30" }); - const value = await redis.hgetdel(key, "name"); - expect(value).toBe("John"); + const values = await redis.hgetdel(key, "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); const check = await redis.hget(key, "name"); expect(check).toBeNull(); @@ -4158,36 +4158,293 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(age).toBe("30"); }); + test("should get and delete multiple hash fields using hgetdel", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const values = await redis.hgetdel(key, "FIELDS", 2, "name", "city"); + expect(values).toEqual(["John", "NYC"]); + + expect(await redis.hget(key, "name")).toBeNull(); + expect(await redis.hget(key, "city")).toBeNull(); + expect(await redis.hget(key, "age")).toBe("30"); + }); + test("should get hash field with expiration using hgetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John" }); - const value = await redis.hgetex(key, "name", "EX", 10); - expect(value).toBe("John"); + const values = await redis.hgetex(key, "EX", 10, "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); const check = await redis.hget(key, "name"); expect(check).toBe("John"); - const ttl = await redis.ttl(key); - expect(ttl).toBeGreaterThan(0); - expect(ttl).toBeLessThanOrEqual(10); + const ttls = await redis.httl(key, "FIELDS", 1, "name"); + expect(ttls).toHaveLength(1); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(10); + }); + + test("should get hash fields without expiration using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const values = await redis.hgetex(key, "FIELDS", 2, "name", "age"); + expect(values).toEqual(["John", "30"]); + }); + + test("should get hash fields with PX expiration using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const values = await redis.hgetex(key, "PX", 5000, "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); + + const ttls = await redis.hpttl(key, "FIELDS", 1, "name"); + expect(ttls).toHaveLength(1); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(5000); + }); + + test("should get hash fields with EXAT using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; + const values = await redis.hgetex(key, "EXAT", futureTimestamp, "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); + }); + + test("should get hash fields with PXAT using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const futureTimestamp = Date.now() + 60000; + const values = await redis.hgetex(key, "PXAT", futureTimestamp, "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); + }); + + test("should get hash fields with PERSIST using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); + + const values = await redis.hgetex(key, "PERSIST", "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); + }); + + test("should get multiple hash fields and return null for missing fields using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const values = await redis.hgetex(key, "FIELDS", 3, "name", "age", "city"); + expect(values).toEqual(["John", null, null]); }); test("should set hash field with expiration using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); - const result = await redis.hsetex(key, "name", "John", "EX", 10); - expect(result).toBe("OK"); + const result = await redis.hsetex(key, "EX", 10, "FIELDS", 1, "name", "John"); + expect(result).toBe(1); const value = await redis.hget(key, "name"); expect(value).toBe("John"); - const ttl = await redis.ttl(key); - expect(ttl).toBeGreaterThan(0); - expect(ttl).toBeLessThanOrEqual(10); + const ttls = await redis.httl(key, "FIELDS", 1, "name"); + expect(ttls).toHaveLength(1); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(10); + }); + + test("should set multiple hash fields with expiration using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hsetex(key, "EX", 10, "FIELDS", 2, "name", "John", "age", "30"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBe("30"); + + const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); + expect(ttls).toHaveLength(2); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(10); + expect(ttls[1]).toBeGreaterThan(0); + expect(ttls[1]).toBeLessThanOrEqual(10); + }); + + test("should set hash fields without expiration using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hsetex(key, "FIELDS", 2, "name", "John", "age", "30"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBe("30"); + + const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); + expect(ttls).toEqual([-1, -1]); + }); + + test("should set hash fields with PX (milliseconds) using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hsetex(key, "PX", 5000, "FIELDS", 1, "name", "John"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + }); + + test("should set hash fields with EXAT (unix timestamp seconds) using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; // 60 seconds from now + const result = await redis.hsetex(key, "EXAT", futureTimestamp, "FIELDS", 1, "name", "John"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + }); + + test("should set hash fields with PXAT (unix timestamp milliseconds) using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const futureTimestamp = Date.now() + 60000; // 60 seconds from now + const result = await redis.hsetex(key, "PXAT", futureTimestamp, "FIELDS", 1, "name", "John"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + }); + + test("should set hash fields with KEEPTTL using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); + + const result = await redis.hsetex(key, "KEEPTTL", "FIELDS", 1, "name", "Jane"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("Jane"); + }); + + test("should set hash fields with FNX flag using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const result1 = await redis.hsetex(key, "FNX", "FIELDS", 2, "name", "Jane", "age", "30"); + expect(result1).toBe(0); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBeNull(); + + const result2 = await redis.hsetex(key, "FNX", "FIELDS", 2, "city", "NYC", "country", "USA"); + expect(result2).toBe(1); + + expect(await redis.hget(key, "city")).toBe("NYC"); + expect(await redis.hget(key, "country")).toBe("USA"); + }); + + test("should set hash fields with FXX flag using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const result1 = await redis.hsetex(key, "FXX", "FIELDS", 2, "name", "Jane", "age", "30"); + expect(result1).toBe(0); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBeNull(); + + await redis.hset(key, { age: "25" }); + const result2 = await redis.hsetex(key, "FXX", "FIELDS", 2, "name", "Jane", "age", "30"); + expect(result2).toBe(1); + + expect(await redis.hget(key, "name")).toBe("Jane"); + expect(await redis.hget(key, "age")).toBe("30"); + }); + + test("should set hash fields with FNX and EX combined using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result1 = await redis.hsetex(key, "FNX", "EX", 10, "FIELDS", 2, "name", "John", "age", "30"); + expect(result1).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBe("30"); + + const result2 = await redis.hsetex(key, "FNX", "EX", 10, "FIELDS", 2, "name", "Jane", "age", "35"); + expect(result2).toBe(0); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBe("30"); + }); + + test("should set hash fields with FXX and PX combined using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const result = await redis.hsetex(key, "FXX", "PX", 5000, "FIELDS", 2, "name", "Jane", "age", "35"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("Jane"); + expect(await redis.hget(key, "age")).toBe("35"); + }); + + test("should check TTL of hash fields using httl", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); + await redis.hset(key, { age: "30" }); + + const ttls = await redis.httl(key, "FIELDS", 3, "name", "age", "nonexistent"); + expect(ttls).toHaveLength(3); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(100); + expect(ttls[1]).toBe(-1); + expect(ttls[2]).toBe(-2); + }); + + test("should check TTL of hash fields using hpttl in milliseconds", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hpexpire(key, 5000, "FIELDS", 1, "name"); + await redis.hset(key, { name: "John", age: "30" }); + + const ttls = await redis.hpttl(key, "FIELDS", 2, "name", "age"); + expect(ttls).toHaveLength(2); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(5000); + expect(ttls[1]).toBe(-1); }); test("should delete hash fields using hdel", async () => { From f48dd342f08fd0c6821010691c86a98b91f62507 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 01:05:53 -0700 Subject: [PATCH 26/45] accident --- packages/bun-types/redis.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index f527731982c7..abd60529c42c 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -1094,7 +1094,7 @@ declare module "bun" { */ lpos( key: RedisClient.KeyLike, - element: RedisClient.ValueLike, + element: RedisClient.KeyLike, ...options: (string | number)[] ): Promise; From 02fb857431eac117de75c729bfe506ecbf930cef Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 01:11:49 -0700 Subject: [PATCH 27/45] ci: fix redis image/compose --- test/docker/docker-compose.yml | 9 ++++----- test/js/valkey/docker-unified/Dockerfile | 5 ++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/test/docker/docker-compose.yml b/test/docker/docker-compose.yml index c6618d48d6ad..465ee5a147b8 100644 --- a/test/docker/docker-compose.yml +++ b/test/docker/docker-compose.yml @@ -153,8 +153,6 @@ services: context: ../js/valkey/docker-unified dockerfile: Dockerfile image: bun-redis-unified:local - environment: - SKIP_FIX_PERMS: "1" ports: - target: 6379 published: 0 @@ -165,9 +163,8 @@ services: protocol: tcp name: tls volumes: - - redis-unix:/tmp/redis:rw - tmpfs: - - /data + - redis-unix:/tmp/redis + - redis-data:/data # MinIO (S3) Service minio: @@ -211,6 +208,8 @@ services: volumes: redis-unix: + redis-data: + driver: local networks: default: diff --git a/test/js/valkey/docker-unified/Dockerfile b/test/js/valkey/docker-unified/Dockerfile index 9af9112d6641..8518dc934d37 100644 --- a/test/js/valkey/docker-unified/Dockerfile +++ b/test/js/valkey/docker-unified/Dockerfile @@ -10,6 +10,7 @@ RUN apk add --no-cache bash # Create directories RUN mkdir -p /etc/redis/certs RUN mkdir -p /docker-entrypoint-initdb.d +RUN mkdir -p /data/appendonlydir && chown -R redis:redis /data # Copy certificates COPY server.key /etc/redis/certs/ @@ -25,7 +26,9 @@ RUN chmod +x /docker-entrypoint-initdb.d/init-redis.sh # Expose ports EXPOSE 6379 6380 -WORKDIR /docker-entrypoint-initdb.d +WORKDIR /data + +USER redis # Use custom entrypoint to run initialization script CMD ["redis-server", "/etc/redis/redis.conf"] \ No newline at end of file From 177d38461799c42166f2761687c7043f1e0f3818 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 01:13:33 -0700 Subject: [PATCH 28/45] fix: hpexpire should come after key exists --- test/js/valkey/valkey.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 3b9dde99b902..49368643727f 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -4437,8 +4437,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); - await redis.hpexpire(key, 5000, "FIELDS", 1, "name"); await redis.hset(key, { name: "John", age: "30" }); + await redis.hpexpire(key, 5000, "FIELDS", 1, "name"); const ttls = await redis.hpttl(key, "FIELDS", 2, "name", "age"); expect(ttls).toHaveLength(2); From 56c3f1f3af4b686106b52135aefb6c1718c12be0 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 01:14:30 -0700 Subject: [PATCH 29/45] feat: add expectation for hpexpire result --- test/js/valkey/valkey.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 49368643727f..4d2f1e8a2b5e 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -4438,7 +4438,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); - await redis.hpexpire(key, 5000, "FIELDS", 1, "name"); + + const expireResult = await redis.hpexpire(key, 5000, "FIELDS", 1, "name"); + expect(expireResult).toEqual([1]); const ttls = await redis.hpttl(key, "FIELDS", 2, "name", "age"); expect(ttls).toHaveLength(2); From 2afb2c2871f92edd0a6673da378d86bb300c32fd Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 01:15:27 -0700 Subject: [PATCH 30/45] rm --- redis.md | 134 ------------------------------------------------------- 1 file changed, 134 deletions(-) delete mode 100644 redis.md diff --git a/redis.md b/redis.md deleted file mode 100644 index 601a00ba76cc..000000000000 --- a/redis.md +++ /dev/null @@ -1,134 +0,0 @@ -# Redis Commands Implementation Progress - -## Core String Operations (11 commands) - -- [x] GETRANGE -- [x] SETRANGE -- [x] SETEX -- [x] PSETEX -- [x] GETBIT -- [x] SETBIT -- [x] INCRBY -- [x] INCRBYFLOAT -- [x] DECRBY -- [x] MSET -- [x] MSETNX - -## List Operations (15 commands) - -- [x] LRANGE -- [x] LINDEX -- [x] LSET -- [x] LINSERT -- [x] LREM -- [x] LTRIM -- [x] RPOPLPUSH -- [x] LPOS -- [x] LMOVE -- [x] BLPOP -- [x] BRPOP -- [x] BLMOVE -- [x] BRPOPLPUSH -- [x] LMPOP -- [x] BLMPOP - -## Set Operations (9 commands) - -- [x] SINTER -- [x] SINTERSTORE -- [x] SINTERCARD -- [x] SUNION -- [x] SUNIONSTORE -- [x] SDIFF -- [x] SDIFFSTORE -- [x] SMISMEMBER -- [x] SSCAN - -## Sorted Set Operations (28 commands) - -### Tier 1 - Simple (6 commands) -- [x] ZINCRBY -- [x] ZCOUNT -- [x] ZLEXCOUNT -- [x] ZREMRANGEBYRANK -- [x] ZREMRANGEBYSCORE -- [x] ZREMRANGEBYLEX - -### Tier 2 - Simple Variadic (2 commands) -- [x] ZREM -- [x] ZMSCORE - -### Tier 3 - Complex Options (9 commands) -- [x] ZADD -- [x] ZRANGE -- [x] ZREVRANGE -- [x] ZRANGEBYSCORE -- [x] ZREVRANGEBYSCORE -- [x] ZRANGEBYLEX -- [x] ZREVRANGEBYLEX -- [x] ZRANGESTORE -- [x] ZSCAN - -### Tier 4 - Set Operations (7 commands) -- [x] ZINTER -- [x] ZINTERSTORE -- [x] ZINTERCARD -- [x] ZDIFF -- [x] ZDIFFSTORE -- [x] ZUNION -- [x] ZUNIONSTORE - -### Tier 5 - Blocking (4 commands) -- [x] BZPOPMIN -- [x] BZPOPMAX -- [x] ZMPOP -- [x] BZMPOP - -## Hash Operations (18 commands) - -### Tier 1 - Ergonomic Set Operations (2 commands) -- [ ] HSET (supports Map/Record OR variadic field/value pairs) -- [ ] HSETNX - -### Tier 2 - Basic Operations (4 commands) -- [ ] HDEL -- [ ] HEXISTS -- [ ] HRANDFIELD -- [ ] HSCAN - -### Tier 3 - Get/Set with Options (3 commands) -- [ ] HGETDEL -- [ ] HGETEX -- [ ] HSETEX - -### Tier 4 - Field Expiration (9 commands) -- [ ] HEXPIRE -- [ ] HEXPIREAT -- [ ] HEXPIRETIME -- [ ] HPERSIST -- [ ] HPEXPIRE -- [ ] HPEXPIREAT -- [ ] HPEXPIRETIME -- [ ] HPTTL -- [ ] HTTL - -## Key Management (8 commands) - -- [x] RENAME -- [x] RENAMENX -- [x] COPY -- [x] UNLINK -- [x] TYPE -- [x] TOUCH -- [x] RANDOMKEY -- [x] SCAN - -## Key Expiration & TTL (3 commands) - -- [x] EXPIREAT -- [x] PEXPIRE -- [x] PEXPIREAT - ---- - -**Total: 92 commands** \ No newline at end of file From 4c976cb905ffa0e5f29f003ffb9855614c85cc05 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 01:49:49 -0700 Subject: [PATCH 31/45] address the primary coderabbit feedback --- packages/bun-types/redis.d.ts | 2 +- src/bun.js/api/valkey.classes.ts | 35 ++++++++++++++++++---- src/valkey/js_valkey_functions.zig | 48 +++++++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index abd60529c42c..1312447b7ce8 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -1851,7 +1851,7 @@ declare module "bun" { * Get the score associated with the given member in a sorted set * @param key The sorted set key * @param member The member to get the score for - * @returns Promise that resolves with the score of the member as a string, + * @returns Promise that resolves with the score of the member as a number, * or null if the member or key doesn't exist */ zscore(key: RedisClient.KeyLike, member: string): Promise; diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 8ac41b2e32e3..07d8e16a4301 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -237,15 +237,19 @@ export default [ }, getbit: { fn: "getbit", + length: 2, }, setbit: { fn: "setbit", + length: 3, }, getrange: { fn: "getrange", + length: 3, }, setrange: { fn: "setrange", + length: 3, }, dump: { fn: "dump", @@ -331,6 +335,7 @@ export default [ }, randomkey: { fn: "randomkey", + length: 0, }, rpop: { fn: "rpop", @@ -386,15 +391,18 @@ export default [ }, type: { fn: "type", + length: 1, }, zcard: { fn: "zcard", }, zcount: { fn: "zcount", + length: 3, }, zlexcount: { fn: "zlexcount", + length: 3, }, zpopmax: { fn: "zpopmax", @@ -421,6 +429,22 @@ export default [ fn: "zrangestore", length: 4, }, + zrem: { + fn: "zrem", + length: 2, + }, + zremrangebylex: { + fn: "zremrangebylex", + length: 3, + }, + zremrangebyrank: { + fn: "zremrangebyrank", + length: 3, + }, + zremrangebyscore: { + fn: "zremrangebyscore", + length: 3, + }, zrevrange: { fn: "zrevrange", length: 3, @@ -459,15 +483,18 @@ export default [ }, setex: { fn: "setex", + length: 3, }, psetex: { fn: "psetex", + length: 3, }, zscore: { fn: "zscore", }, zincrby: { fn: "zincrby", + length: 3, }, zmscore: { fn: "zmscore", @@ -542,10 +569,6 @@ export default [ substr: { fn: "substr" }, hstrlen: { fn: "hstrlen" }, zrank: { fn: "zrank" }, - zrem: { fn: "zrem" }, - zremrangebylex: { fn: "zremrangebylex" }, - zremrangebyrank: { fn: "zremrangebyrank" }, - zremrangebyscore: { fn: "zremrangebyscore" }, zrevrank: { fn: "zrevrank" }, subscribe: { fn: "subscribe" }, duplicate: { fn: "duplicate" }, @@ -556,8 +579,8 @@ export default [ copy: { fn: "copy" }, unlink: { fn: "unlink" }, touch: { fn: "touch" }, - rename: { fn: "rename" }, - renamenx: { fn: "renamenx" }, + rename: { fn: "rename", length: 2 }, + renamenx: { fn: "renamenx", length: 2 }, }, values: ["onconnect", "onclose", "connectionPromise", "hello", "subscriptionCallbackMap"], }), diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 7181176c5041..4a8d80170006 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -824,11 +824,11 @@ pub const pfadd = compile.@"(key: RedisKey, value: RedisValue)"("pfadd", "PFADD" pub const rpush = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("rpush", "RPUSH", .not_subscriber).call; pub const rpushx = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("rpushx", "RPUSHX", .not_subscriber).call; pub const setnx = compile.@"(key: RedisKey, value: RedisValue)"("setnx", "SETNX", "key", "value", .not_subscriber).call; -pub const setex = compile.@"(...strings: string[])"("setex", "SETEX", .not_subscriber).call; -pub const psetex = compile.@"(...strings: string[])"("psetex", "PSETEX", .not_subscriber).call; +pub const setex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("setex", "SETEX", "key", "seconds", "value", .not_subscriber).call; +pub const psetex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("psetex", "PSETEX", "key", "milliseconds", "value", .not_subscriber).call; pub const zscore = compile.@"(key: RedisKey, value: RedisValue)"("zscore", "ZSCORE", "key", "value", .not_subscriber).call; -pub const zincrby = compile.@"(...strings: string[])"("zincrby", "ZINCRBY", .not_subscriber).call; -pub const zmscore = compile.@"(...strings: string[])"("zmscore", "ZMSCORE", .not_subscriber).call; +pub const zincrby = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zincrby", "ZINCRBY", "key", "increment", "member", .not_subscriber).call; +pub const zmscore = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("zmscore", "ZMSCORE", .not_subscriber).call; pub const zadd = compile.@"(...strings: string[])"("zadd", "ZADD", .not_subscriber).call; pub const zscan = compile.@"(...strings: string[])"("zscan", "ZSCAN", .not_subscriber).call; pub const zdiff = compile.@"(...strings: string[])"("zdiff", "ZDIFF", .not_subscriber).call; @@ -1331,6 +1331,46 @@ const compile = struct { }; } + pub fn @"(key: RedisKey, value: RedisValue, value2: RedisValue)"( + comptime name: []const u8, + comptime command: []const u8, + comptime arg0_name: []const u8, + comptime arg1_name: []const u8, + comptime arg2_name: []const u8, + comptime client_state_requirement: ClientStateRequirement, + ) type { + return struct { + pub fn call(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + try testCorrectState(this, name, client_state_requirement); + + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { + return globalObject.throwInvalidArgumentType(name, arg0_name, "string or buffer"); + }; + defer key.deinit(); + const value = (try fromJS(globalObject, callframe.argument(1))) orelse { + return globalObject.throwInvalidArgumentType(name, arg1_name, "string or buffer"); + }; + defer value.deinit(); + const value2 = (try fromJS(globalObject, callframe.argument(2))) orelse { + return globalObject.throwInvalidArgumentType(name, arg2_name, "string or buffer"); + }; + defer value2.deinit(); + + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = command, + .args = .{ .args = &.{ key, value, value2 } }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send " ++ command, err); + }; + return promise.toJS(); + } + }; + } + pub fn @"(...strings: string[])"( comptime name: []const u8, comptime command: []const u8, From 5de2c78cd92ed600b0b0f3b13fcf067c4cbfdecb Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 02:53:31 -0700 Subject: [PATCH 32/45] use fixed arity for some fns --- src/valkey/js_valkey_functions.zig | 49 ++++++++++++++---------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 4a8d80170006..ad4db1a7e261 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -752,11 +752,11 @@ pub const blmove = compile.@"(...strings: string[])"("blmove", "BLMOVE", .not_su pub const blmpop = compile.@"(...strings: string[])"("blmpop", "BLMPOP", .not_subscriber).call; pub const blpop = compile.@"(...strings: string[])"("blpop", "BLPOP", .not_subscriber).call; pub const brpop = compile.@"(...strings: string[])"("brpop", "BRPOP", .not_subscriber).call; -pub const brpoplpush = compile.@"(...strings: string[])"("brpoplpush", "BRPOPLPUSH", .not_subscriber).call; +pub const brpoplpush = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("brpoplpush", "BRPOPLPUSH", "source", "destination", "timeout", .not_subscriber).call; pub const getbit = compile.@"(key: RedisKey, value: RedisValue)"("getbit", "GETBIT", "key", "offset", .not_subscriber).call; -pub const setbit = compile.@"(...strings: string[])"("setbit", "SETBIT", .not_subscriber).call; -pub const getrange = compile.@"(...strings: string[])"("getrange", "GETRANGE", .not_subscriber).call; -pub const setrange = compile.@"(...strings: string[])"("setrange", "SETRANGE", .not_subscriber).call; +pub const setbit = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("setbit", "SETBIT", "key", "offset", "value", .not_subscriber).call; +pub const getrange = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("getrange", "GETRANGE", "key", "start", "end", .not_subscriber).call; +pub const setrange = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("setrange", "SETRANGE", "key", "offset", "value", .not_subscriber).call; pub const dump = compile.@"(key: RedisKey)"("dump", "DUMP", "key", .not_subscriber).call; pub const expireat = compile.@"(key: RedisKey, value: RedisValue)"("expireat", "EXPIREAT", "key", "timestamp", .not_subscriber).call; pub const expiretime = compile.@"(key: RedisKey)"("expiretime", "EXPIRETIME", "key", .not_subscriber).call; @@ -767,17 +767,17 @@ pub const hkeys = compile.@"(key: RedisKey)"("hkeys", "HKEYS", "key", .not_subsc pub const hlen = compile.@"(key: RedisKey)"("hlen", "HLEN", "key", .not_subscriber).call; pub const hvals = compile.@"(key: RedisKey)"("hvals", "HVALS", "key", .not_subscriber).call; pub const keys = compile.@"(key: RedisKey)"("keys", "KEYS", "key", .not_subscriber).call; -pub const lindex = compile.@"(...strings: string[])"("lindex", "LINDEX", .not_subscriber).call; +pub const lindex = compile.@"(key: RedisKey, value: RedisValue)"("lindex", "LINDEX", "key", "index", .not_subscriber).call; pub const linsert = compile.@"(...strings: string[])"("linsert", "LINSERT", .not_subscriber).call; pub const llen = compile.@"(key: RedisKey)"("llen", "LLEN", "key", .not_subscriber).call; pub const lmove = compile.@"(...strings: string[])"("lmove", "LMOVE", .not_subscriber).call; pub const lmpop = compile.@"(...strings: string[])"("lmpop", "LMPOP", .not_subscriber).call; pub const lpop = compile.@"(key: RedisKey)"("lpop", "LPOP", "key", .not_subscriber).call; pub const lpos = compile.@"(...strings: string[])"("lpos", "LPOS", .not_subscriber).call; -pub const lrange = compile.@"(...strings: string[])"("lrange", "LRANGE", .not_subscriber).call; -pub const lrem = compile.@"(...strings: string[])"("lrem", "LREM", .not_subscriber).call; -pub const lset = compile.@"(...strings: string[])"("lset", "LSET", .not_subscriber).call; -pub const ltrim = compile.@"(...strings: string[])"("ltrim", "LTRIM", .not_subscriber).call; +pub const lrange = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lrange", "LRANGE", "key", "start", "stop", .not_subscriber).call; +pub const lrem = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lrem", "LREM", "key", "count", "element", .not_subscriber).call; +pub const lset = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lset", "LSET", "key", "index", "element", .not_subscriber).call; +pub const ltrim = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("ltrim", "LTRIM", "key", "start", "stop", .not_subscriber).call; pub const persist = compile.@"(key: RedisKey)"("persist", "PERSIST", "key", .not_subscriber).call; pub const pexpire = compile.@"(key: RedisKey, value: RedisValue)"("pexpire", "PEXPIRE", "key", "milliseconds", .not_subscriber).call; pub const pexpireat = compile.@"(key: RedisKey, value: RedisValue)"("pexpireat", "PEXPIREAT", "key", "milliseconds-timestamp", .not_subscriber).call; @@ -785,7 +785,7 @@ pub const pexpiretime = compile.@"(key: RedisKey)"("pexpiretime", "PEXPIRETIME", pub const pttl = compile.@"(key: RedisKey)"("pttl", "PTTL", "key", .not_subscriber).call; pub const randomkey = compile.@"()"("randomkey", "RANDOMKEY", .not_subscriber).call; pub const rpop = compile.@"(key: RedisKey)"("rpop", "RPOP", "key", .not_subscriber).call; -pub const rpoplpush = compile.@"(...strings: string[])"("rpoplpush", "RPOPLPUSH", .not_subscriber).call; +pub const rpoplpush = compile.@"(key: RedisKey, value: RedisValue)"("rpoplpush", "RPOPLPUSH", "source", "destination", .not_subscriber).call; pub const scan = compile.@"(...strings: string[])"("scan", "SCAN", .not_subscriber).call; pub const scard = compile.@"(key: RedisKey)"("scard", "SCARD", "key", .not_subscriber).call; pub const sdiff = compile.@"(...strings: string[])"("sdiff", "SDIFF", .not_subscriber).call; @@ -800,8 +800,8 @@ pub const sunion = compile.@"(...strings: string[])"("sunion", "SUNION", .not_su pub const sunionstore = compile.@"(...strings: string[])"("sunionstore", "SUNIONSTORE", .not_subscriber).call; pub const @"type" = compile.@"(key: RedisKey)"("type", "TYPE", "key", .not_subscriber).call; pub const zcard = compile.@"(key: RedisKey)"("zcard", "ZCARD", "key", .not_subscriber).call; -pub const zcount = compile.@"(...strings: string[])"("zcount", "ZCOUNT", .not_subscriber).call; -pub const zlexcount = compile.@"(...strings: string[])"("zlexcount", "ZLEXCOUNT", .not_subscriber).call; +pub const zcount = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zcount", "ZCOUNT", "key", "min", "max", .not_subscriber).call; +pub const zlexcount = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zlexcount", "ZLEXCOUNT", "key", "min", "max", .not_subscriber).call; pub const zpopmax = compile.@"(key: RedisKey)"("zpopmax", "ZPOPMAX", "key", .not_subscriber).call; pub const zpopmin = compile.@"(key: RedisKey)"("zpopmin", "ZPOPMIN", "key", .not_subscriber).call; pub const zrandmember = compile.@"(key: RedisKey)"("zrandmember", "ZRANDMEMBER", "key", .not_subscriber).call; @@ -809,9 +809,8 @@ pub const zrange = compile.@"(...strings: string[])"("zrange", "ZRANGE", .not_su pub const zrevrange = compile.@"(...strings: string[])"("zrevrange", "ZREVRANGE", .not_subscriber).call; pub const zrangebyscore = compile.@"(...strings: string[])"("zrangebyscore", "ZRANGEBYSCORE", .not_subscriber).call; pub const zrevrangebyscore = compile.@"(...strings: string[])"("zrevrangebyscore", "ZREVRANGEBYSCORE", .not_subscriber).call; -pub const zrangebylex = compile.@"(...strings: string[])"("zrangebylex", "ZRANGEBYLEX", .not_subscriber).call; -pub const zrevrangebylex = compile.@"(...strings: string[])"("zrevrangebylex", "ZREVRANGEBYLEX", .not_subscriber).call; - +pub const zrangebylex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zrangebylex", "ZRANGEBYLEX", "key", "min", "max", .not_subscriber).call; +pub const zrevrangebylex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zrevrangebylex", "ZREVRANGEBYLEX", "key", "max", "min", .not_subscriber).call; pub const append = compile.@"(key: RedisKey, value: RedisValue)"("append", "APPEND", "key", "value", .not_subscriber).call; pub const getset = compile.@"(key: RedisKey, value: RedisValue)"("getset", "GETSET", "key", "value", .not_subscriber).call; pub const hget = compile.@"(key: RedisKey, value: RedisValue)"("hget", "HGET", "key", "field", .not_subscriber).call; @@ -842,25 +841,23 @@ pub const zmpop = compile.@"(...strings: string[])"("zmpop", "ZMPOP", .not_subsc pub const bzmpop = compile.@"(...strings: string[])"("bzmpop", "BZMPOP", .not_subscriber).call; pub const bzpopmin = compile.@"(...strings: string[])"("bzpopmin", "BZPOPMIN", .not_subscriber).call; pub const bzpopmax = compile.@"(...strings: string[])"("bzpopmax", "BZPOPMAX", .not_subscriber).call; - pub const del = compile.@"(key: RedisKey, ...args: RedisKey[])"("del", "DEL", "key", .not_subscriber).call; pub const mget = compile.@"(key: RedisKey, ...args: RedisKey[])"("mget", "MGET", "key", .not_subscriber).call; pub const mset = compile.@"(...strings: string[])"("mset", "MSET", .not_subscriber).call; pub const msetnx = compile.@"(...strings: string[])"("msetnx", "MSETNX", .not_subscriber).call; - pub const script = compile.@"(...strings: string[])"("script", "SCRIPT", .not_subscriber).call; pub const select = compile.@"(...strings: string[])"("select", "SELECT", .not_subscriber).call; -pub const spublish = compile.@"(...strings: string[])"("spublish", "SPUBLISH", .not_subscriber).call; -pub const smove = compile.@"(...strings: string[])"("smove", "SMOVE", .not_subscriber).call; -pub const substr = compile.@"(...strings: string[])"("substr", "SUBSTR", .not_subscriber).call; -pub const hstrlen = compile.@"(...strings: string[])"("hstrlen", "HSTRLEN", .not_subscriber).call; -pub const zrank = compile.@"(...strings: string[])"("zrank", "ZRANK", .not_subscriber).call; +pub const spublish = compile.@"(key: RedisKey, value: RedisValue)"("spublish", "SPUBLISH", "channel", "message", .not_subscriber).call; +pub const smove = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("smove", "SMOVE", "source", "destination", "member", .not_subscriber).call; +pub const substr = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("substr", "SUBSTR", "key", "start", "end", .not_subscriber).call; +pub const hstrlen = compile.@"(key: RedisKey, value: RedisValue)"("hstrlen", "HSTRLEN", "key", "field", .not_subscriber).call; +pub const zrank = compile.@"(key: RedisKey, value: RedisValue)"("zrank", "ZRANK", "key", "member", .not_subscriber).call; pub const zrangestore = compile.@"(...strings: string[])"("zrangestore", "ZRANGESTORE", .not_subscriber).call; pub const zrem = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrem", "ZREM", "key", .not_subscriber).call; -pub const zremrangebylex = compile.@"(...strings: string[])"("zremrangebylex", "ZREMRANGEBYLEX", .not_subscriber).call; -pub const zremrangebyrank = compile.@"(...strings: string[])"("zremrangebyrank", "ZREMRANGEBYRANK", .not_subscriber).call; -pub const zremrangebyscore = compile.@"(...strings: string[])"("zremrangebyscore", "ZREMRANGEBYSCORE", .not_subscriber).call; -pub const zrevrank = compile.@"(...strings: string[])"("zrevrank", "ZREVRANK", .not_subscriber).call; +pub const zremrangebylex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebylex", "ZREMRANGEBYLEX", "key", "min", "max", .not_subscriber).call; +pub const zremrangebyrank = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebyrank", "ZREMRANGEBYRANK", "key", "start", "stop", .not_subscriber).call; +pub const zremrangebyscore = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebyscore", "ZREMRANGEBYSCORE", "key", "min", "max", .not_subscriber).call; +pub const zrevrank = compile.@"(key: RedisKey, value: RedisValue)"("zrevrank", "ZREVRANK", "key", "member", .not_subscriber).call; pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE", .dont_care).call; pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE", .dont_care).call; pub const pubsub = compile.@"(...strings: string[])"("pubsub", "PUBSUB", .dont_care).call; From e6b9e565bc7e431ce450f2936f724a6abb818368 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 02:54:33 -0700 Subject: [PATCH 33/45] update some missing types --- packages/bun-types/redis.d.ts | 97 ++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 1312447b7ce8..8ce3b3597ad2 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -758,6 +758,15 @@ declare module "bun" { */ srem(key: RedisClient.KeyLike, member: string): Promise; + /** + * Move a member from one set to another + * @param source The source set key + * @param destination The destination set key + * @param member The member to move + * @returns Promise that resolves with true if the element was moved, false if it wasn't a member of source + */ + smove(source: RedisClient.KeyLike, destination: RedisClient.KeyLike, member: string): Promise; + /** * Get all the members in a set * @param key The set key @@ -773,6 +782,14 @@ declare module "bun" { */ srandmember(key: RedisClient.KeyLike): Promise; + /** + * Get count random members from a set + * @param key The set key + * @returns Promise that resolves with an array of up to count random members, or null if the set + * doesn't exist + */ + srandmember(key: RedisClient.KeyLike, count: number): Promise; + /** * Remove and return a random member from a set * @param key The set key @@ -781,6 +798,22 @@ declare module "bun" { */ spop(key: RedisClient.KeyLike): Promise; + /** + * Remove and return count members from the set + * @param key The set key + * @returns Promise that resolves with the removed members, or null if the + * set is empty + */ + spop(key: RedisClient.KeyLike, count: number): Promise; + + /** + * Post a message to a shard channel + * @param channel The shard channel name + * @param message The message to publish + * @returns Promise that resolves with the number of clients that received the message + */ + spublish(channel: RedisClient.KeyLike, message: string): Promise; + /** * Store the difference of multiple sets in a key * @param destination The destination key to store the result @@ -1063,6 +1096,13 @@ declare module "bun" { */ lpop(key: RedisClient.KeyLike): Promise; + /** + * Remove and get the first count elements in a list + * @param key The list key + * @returns Promise that resolves with a list of elements, or null if the list doesn't exist + */ + lpop(key: RedisClient.KeyLike, count: number): Promise; + /** * Find the position(s) of an element in a list * @@ -1469,18 +1509,20 @@ declare module "bun" { /** * Remove and return members with the highest scores in a sorted set * @param key The sorted set key - * @returns Promise that resolves with the removed member and its score, or - * null if the set is empty + * @param count Optional number of members to pop (default: 1) + * @returns Promise that resolves with array of [member, score] tuples, or + * empty array if the set is empty */ - zpopmax(key: RedisClient.KeyLike): Promise; + zpopmax(key: RedisClient.KeyLike, count?: number): Promise>; /** * Remove and return members with the lowest scores in a sorted set * @param key The sorted set key - * @returns Promise that resolves with the removed member and its score, or - * null if the set is empty + * @param count Optional number of members to pop (default: 1) + * @returns Promise that resolves with array of [member, score] tuples, or + * empty array if the set is empty */ - zpopmin(key: RedisClient.KeyLike): Promise; + zpopmin(key: RedisClient.KeyLike, count?: number): Promise<[string, number][]>; /** * Remove and return the member with the lowest score from one or more sorted sets, or block until one is available @@ -1522,6 +1564,22 @@ declare module "bun" { */ zrandmember(key: RedisClient.KeyLike): Promise; + /** + * Get one or multiple random members from a sorted set + * @param key The sorted set key + * @returns Promise that resolves with a random member, or null if the set + * is empty + */ + zrandmember(key: RedisClient.KeyLike, count: number): Promise; + + /** + * Get one or multiple random members from a sorted set, with scores + * @param key The sorted set key + * @returns Promise that resolves with a random member, or null if the set + * is empty + */ + zrandmember(key: RedisClient.KeyLike, count: number, withscores: "WITHSCORES"): Promise<[string, number][] | null>; + /** * Return a range of members in a sorted set with their scores * @@ -2361,6 +2419,22 @@ declare module "bun" { ...options: string[] ): Promise; + /** + * Determine the index of a member in a sorted set + * @param key The sorted set key + * @param member The member to find + * @returns Promise that resolves with the rank (index) of the member, or null if the member doesn't exist + */ + zrank(key: RedisClient.KeyLike, member: string): Promise; + + /** + * Determine the index of a member in a sorted set, with scores ordered from high to low + * @param key The sorted set key + * @param member The member to find + * @returns Promise that resolves with the rank (index) of the member, or null if the member doesn't exist + */ + zrevrank(key: RedisClient.KeyLike, member: string): Promise; + /** * Get the values of all specified keys * @param keys The keys to get @@ -2451,6 +2525,17 @@ declare module "bun" { */ getrange(key: RedisClient.KeyLike, start: number, end: number): Promise; + /** + * Get a substring of the string stored at a key + * @param key The key to retrieve from + * @param start The starting offset + * @param end The ending offset + * @returns Promise that resolves with the substring value + * + * @deprecated Use {@link getrange} instead. SUBSTR is a deprecated Redis command. + */ + substr(key: RedisClient.KeyLike, start: number, end: number): Promise; + /** * Overwrite part of a string at key starting at the specified offset * @param key The key to modify From 25036140c276460a38537d42016d971bed387bce Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 03:35:41 -0700 Subject: [PATCH 34/45] update variadic methods --- packages/bun-types/redis.d.ts | 38 +++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 8ce3b3597ad2..57dcbfbf44de 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -631,7 +631,7 @@ declare module "bun" { * @param fields The fields to get * @returns Promise that resolves with an array of values */ - hmget(key: RedisClient.KeyLike, fields: string[]): Promise>; + hmget(key: RedisClient.KeyLike, ...fields: string[]): Promise>; /** * Delete one or more hash fields @@ -741,22 +741,20 @@ declare module "bun" { sismember(key: RedisClient.KeyLike, member: string): Promise; /** - * Add a member to a set + * Add one or more members to a set * @param key The set key - * @param member The member to add - * @returns Promise that resolves with 1 if the member was added, 0 if it - * already existed + * @param members The members to add + * @returns Promise that resolves with the number of members added */ - sadd(key: RedisClient.KeyLike, member: string): Promise; + sadd(key: RedisClient.KeyLike, ...members: string[]): Promise; /** - * Remove a member from a set + * Remove one or more members from a set * @param key The set key - * @param member The member to remove - * @returns Promise that resolves with 1 if the member was removed, 0 if it - * didn't exist + * @param members The members to remove + * @returns Promise that resolves with the number of members removed */ - srem(key: RedisClient.KeyLike, member: string): Promise; + srem(key: RedisClient.KeyLike, ...members: string[]): Promise; /** * Move a member from one set to another @@ -2427,6 +2425,15 @@ declare module "bun" { */ zrank(key: RedisClient.KeyLike, member: string): Promise; + /** + * Determine the index of a member in a sorted set with score + * @param key The sorted set key + * @param member The member to find + * @param withscore "WITHSCORE" to include the score + * @returns Promise that resolves with [rank, score] or null if the member doesn't exist + */ + zrank(key: RedisClient.KeyLike, member: string, withscore: "WITHSCORE"): Promise<[number, number] | null>; + /** * Determine the index of a member in a sorted set, with scores ordered from high to low * @param key The sorted set key @@ -2435,6 +2442,15 @@ declare module "bun" { */ zrevrank(key: RedisClient.KeyLike, member: string): Promise; + /** + * Determine the index of a member in a sorted set with score, with scores ordered from high to low + * @param key The sorted set key + * @param member The member to find + * @param withscore "WITHSCORE" to include the score + * @returns Promise that resolves with [rank, score] or null if the member doesn't exist + */ + zrevrank(key: RedisClient.KeyLike, member: string, withscore: "WITHSCORE"): Promise<[number, number] | null>; + /** * Get the values of all specified keys * @param keys The keys to get From 62e0eda8736086b863fd88590bccbe40a62c8a8c Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 03:49:00 -0700 Subject: [PATCH 35/45] update sorted set types --- packages/bun-types/redis.d.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 57dcbfbf44de..5713c689c213 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -1504,23 +1504,37 @@ declare module "bun" { */ zlexcount(key: RedisClient.KeyLike, min: string, max: string): Promise; + /** + * Remove and return members with the highest scores in a sorted set + * @param key The sorted set key + * @returns Promise that resolves with either [member, score] or empty + * array if the set is empty + */ + zpopmax(key: RedisClient.KeyLike): Promise<[string, number] | []>; + /** * Remove and return members with the highest scores in a sorted set * @param key The sorted set key * @param count Optional number of members to pop (default: 1) + * @returns Promise that resolves with an array of [member, score] tuples + */ + zpopmax(key: RedisClient.KeyLike, count: number): Promise>; + + /** + * Remove and return members with the lowest scores in a sorted set + * @param key The sorted set key * @returns Promise that resolves with array of [member, score] tuples, or * empty array if the set is empty */ - zpopmax(key: RedisClient.KeyLike, count?: number): Promise>; + zpopmin(key: RedisClient.KeyLike): Promise<[string, number] | []>; /** * Remove and return members with the lowest scores in a sorted set * @param key The sorted set key * @param count Optional number of members to pop (default: 1) - * @returns Promise that resolves with array of [member, score] tuples, or - * empty array if the set is empty + * @returns Promise that resolves with an array of [member, score] tuples */ - zpopmin(key: RedisClient.KeyLike, count?: number): Promise<[string, number][]>; + zpopmin(key: RedisClient.KeyLike, count: number): Promise<[string, number][]>; /** * Remove and return the member with the lowest score from one or more sorted sets, or block until one is available From 53cd46cac96cc9118e7ffa1abcb4c8033d09ec63 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 03:55:31 -0700 Subject: [PATCH 36/45] a bunch more coverage --- packages/bun-types/redis.d.ts | 8 + src/valkey/js_valkey_functions.zig | 204 +- test/js/valkey/valkey.test.ts | 3180 ++++++++++++++++++++++++---- 3 files changed, 2945 insertions(+), 447 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 5713c689c213..2960792bfebb 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -886,6 +886,14 @@ declare module "bun" { */ hlen(key: RedisClient.KeyLike): Promise; + /** + * Get the string length of the value stored in a hash field + * @param key The hash key + * @param field The field name + * @returns Promise that resolves with the length of the string value, or 0 if the field doesn't exist + */ + hstrlen(key: RedisClient.KeyLike, field: string): Promise; + /** * Get all values in a hash * @param key The hash key diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index ad4db1a7e261..f91aaf9ca2e4 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -274,14 +274,34 @@ pub fn ttl(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: pub fn srem(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.throw("SREM requires at least a key and one member", .{}); + } + + var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); + var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len); + defer { + for (args.items) |*item| { + item.deinit(); + } + args.deinit(); + } + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { return globalObject.throwInvalidArgumentType("srem", "key", "string or buffer"); }; - defer key.deinit(); - const value = (try fromJS(globalObject, callframe.argument(1))) orelse { - return globalObject.throwInvalidArgumentType("srem", "value", "string or buffer"); - }; - defer value.deinit(); + args.appendAssumeCapacity(key); + + for (args_view[1..]) |arg| { + if (arg.isUndefinedOrNull()) { + break; + } + const value = (try fromJS(globalObject, arg)) orelse { + return globalObject.throwInvalidArgumentType("srem", "member", "string or buffer"); + }; + args.appendAssumeCapacity(value); + } // Send SREM command const promise = this.send( @@ -289,7 +309,7 @@ pub fn srem(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: callframe.this(), &.{ .command = "SREM", - .args = .{ .args = &.{ key, value } }, + .args = .{ .args = args.items }, }, ) catch |err| { return protocol.valkeyErrorToJS(globalObject, "Failed to send SREM command", err); @@ -301,10 +321,36 @@ pub fn srem(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: pub fn srandmember(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, @src().fn_name); + const args_view = callframe.arguments(); + var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); + var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len); + defer { + for (args.items) |*item| { + item.deinit(); + } + args.deinit(); + } + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { return globalObject.throwInvalidArgumentType("srandmember", "key", "string or buffer"); }; - defer key.deinit(); + args.appendAssumeCapacity(key); + + // Optional count argument + if (args_view.len > 1 and !callframe.argument(1).isUndefinedOrNull()) { + const count_arg = try fromJS(globalObject, callframe.argument(1)) orelse { + return globalObject.throwInvalidArgumentType("srandmember", "count", "number or string"); + }; + args.appendAssumeCapacity(count_arg); + + // Optional WITHSCORES argument + if (args_view.len > 2 and !callframe.argument(2).isUndefinedOrNull()) { + const withscores_arg = try fromJS(globalObject, callframe.argument(2)) orelse { + return globalObject.throwInvalidArgumentType("srandmember", "withscores", "string"); + }; + args.appendAssumeCapacity(withscores_arg); + } + } // Send SRANDMEMBER command const promise = this.send( @@ -312,7 +358,7 @@ pub fn srandmember(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, cal callframe.this(), &.{ .command = "SRANDMEMBER", - .args = .{ .args = &.{key} }, + .args = .{ .args = args.items }, }, ) catch |err| { return protocol.valkeyErrorToJS(globalObject, "Failed to send SRANDMEMBER command", err); @@ -347,10 +393,28 @@ pub fn smembers(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callfr pub fn spop(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, @src().fn_name); + const args_view = callframe.arguments(); + var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); + var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len); + defer { + for (args.items) |*item| { + item.deinit(); + } + args.deinit(); + } + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { return globalObject.throwInvalidArgumentType("spop", "key", "string or buffer"); }; - defer key.deinit(); + args.appendAssumeCapacity(key); + + // Optional count argument + if (args_view.len > 1 and !callframe.argument(1).isUndefinedOrNull()) { + const count_arg = try fromJS(globalObject, callframe.argument(1)) orelse { + return globalObject.throwInvalidArgumentType("spop", "count", "number or string"); + }; + args.appendAssumeCapacity(count_arg); + } // Send SPOP command const promise = this.send( @@ -358,7 +422,7 @@ pub fn spop(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: callframe.this(), &.{ .command = "SPOP", - .args = .{ .args = &.{key} }, + .args = .{ .args = args.items }, }, ) catch |err| { return protocol.valkeyErrorToJS(globalObject, "Failed to send SPOP command", err); @@ -370,14 +434,34 @@ pub fn spop(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: pub fn sadd(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.throw("SADD requires at least a key and one member", .{}); + } + + var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); + var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len); + defer { + for (args.items) |*item| { + item.deinit(); + } + args.deinit(); + } + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { return globalObject.throwInvalidArgumentType("sadd", "key", "string or buffer"); }; - defer key.deinit(); - const value = (try fromJS(globalObject, callframe.argument(1))) orelse { - return globalObject.throwInvalidArgumentType("sadd", "value", "string or buffer"); - }; - defer value.deinit(); + args.appendAssumeCapacity(key); + + for (args_view[1..]) |arg| { + if (arg.isUndefinedOrNull()) { + break; + } + const value = (try fromJS(globalObject, arg)) orelse { + return globalObject.throwInvalidArgumentType("sadd", "member", "string or buffer"); + }; + args.appendAssumeCapacity(value); + } // Send SADD command const promise = this.send( @@ -385,7 +469,7 @@ pub fn sadd(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: callframe.this(), &.{ .command = "SADD", - .args = .{ .args = &.{ key, value } }, + .args = .{ .args = args.items }, }, ) catch |err| { return protocol.valkeyErrorToJS(globalObject, "Failed to send SADD command", err); @@ -425,35 +509,33 @@ pub fn sismember(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callf pub fn hmget(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, @src().fn_name); - const key = (try fromJS(globalObject, callframe.argument(0))) orelse { - return globalObject.throwInvalidArgumentType("hmget", "key", "string or buffer"); - }; - defer key.deinit(); - - // Get field array argument - const fields_array = callframe.argument(1); - if (!fields_array.isObject() or !fields_array.isArray()) { - return globalObject.throw("Fields must be an array", .{}); + const args_view = callframe.arguments(); + if (args_view.len < 2) { + return globalObject.throw("HMGET requires at least a key and one field", .{}); } - var iter = try fields_array.arrayIterator(globalObject); - var args = try std.ArrayList(jsc.ZigString.Slice).initCapacity(bun.default_allocator, iter.len + 1); + var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); + var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len); defer { - for (args.items) |item| { + for (args.items) |*item| { item.deinit(); } args.deinit(); } - args.appendAssumeCapacity(jsc.ZigString.Slice.fromUTF8NeverFree(key.slice())); - - // Add field names as arguments - while (try iter.next()) |field_js| { - const field_str = try field_js.toBunString(globalObject); - defer field_str.deref(); + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { + return globalObject.throwInvalidArgumentType("hmget", "key", "string or buffer"); + }; + args.appendAssumeCapacity(key); - const field_slice = field_str.toUTF8WithoutRef(bun.default_allocator); - args.appendAssumeCapacity(field_slice); + for (args_view[1..]) |arg| { + if (arg.isUndefinedOrNull()) { + break; + } + const field = (try fromJS(globalObject, arg)) orelse { + return globalObject.throwInvalidArgumentType("hmget", "field", "string or buffer"); + }; + args.appendAssumeCapacity(field); } // Send HMGET command @@ -462,7 +544,7 @@ pub fn hmget(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe callframe.this(), &.{ .command = "HMGET", - .args = .{ .slices = args.items }, + .args = .{ .args = args.items }, }, ) catch |err| { return protocol.valkeyErrorToJS(globalObject, "Failed to send HMGET command", err); @@ -772,7 +854,7 @@ pub const linsert = compile.@"(...strings: string[])"("linsert", "LINSERT", .not pub const llen = compile.@"(key: RedisKey)"("llen", "LLEN", "key", .not_subscriber).call; pub const lmove = compile.@"(...strings: string[])"("lmove", "LMOVE", .not_subscriber).call; pub const lmpop = compile.@"(...strings: string[])"("lmpop", "LMPOP", .not_subscriber).call; -pub const lpop = compile.@"(key: RedisKey)"("lpop", "LPOP", "key", .not_subscriber).call; +pub const lpop = compile.@"(key: RedisKey, ...args: RedisKey[])"("lpop", "LPOP", "key", .not_subscriber).call; pub const lpos = compile.@"(...strings: string[])"("lpos", "LPOS", .not_subscriber).call; pub const lrange = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lrange", "LRANGE", "key", "start", "stop", .not_subscriber).call; pub const lrem = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lrem", "LREM", "key", "count", "element", .not_subscriber).call; @@ -784,7 +866,7 @@ pub const pexpireat = compile.@"(key: RedisKey, value: RedisValue)"("pexpireat", pub const pexpiretime = compile.@"(key: RedisKey)"("pexpiretime", "PEXPIRETIME", "key", .not_subscriber).call; pub const pttl = compile.@"(key: RedisKey)"("pttl", "PTTL", "key", .not_subscriber).call; pub const randomkey = compile.@"()"("randomkey", "RANDOMKEY", .not_subscriber).call; -pub const rpop = compile.@"(key: RedisKey)"("rpop", "RPOP", "key", .not_subscriber).call; +pub const rpop = compile.@"(key: RedisKey, ...args: RedisKey[])"("rpop", "RPOP", "key", .not_subscriber).call; pub const rpoplpush = compile.@"(key: RedisKey, value: RedisValue)"("rpoplpush", "RPOPLPUSH", "source", "destination", .not_subscriber).call; pub const scan = compile.@"(...strings: string[])"("scan", "SCAN", .not_subscriber).call; pub const scard = compile.@"(key: RedisKey)"("scard", "SCARD", "key", .not_subscriber).call; @@ -802,15 +884,15 @@ pub const @"type" = compile.@"(key: RedisKey)"("type", "TYPE", "key", .not_subsc pub const zcard = compile.@"(key: RedisKey)"("zcard", "ZCARD", "key", .not_subscriber).call; pub const zcount = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zcount", "ZCOUNT", "key", "min", "max", .not_subscriber).call; pub const zlexcount = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zlexcount", "ZLEXCOUNT", "key", "min", "max", .not_subscriber).call; -pub const zpopmax = compile.@"(key: RedisKey)"("zpopmax", "ZPOPMAX", "key", .not_subscriber).call; -pub const zpopmin = compile.@"(key: RedisKey)"("zpopmin", "ZPOPMIN", "key", .not_subscriber).call; -pub const zrandmember = compile.@"(key: RedisKey)"("zrandmember", "ZRANDMEMBER", "key", .not_subscriber).call; +pub const zpopmax = compile.@"(key: RedisKey, ...args: RedisKey[])"("zpopmax", "ZPOPMAX", "key", .not_subscriber).call; +pub const zpopmin = compile.@"(key: RedisKey, ...args: RedisKey[])"("zpopmin", "ZPOPMIN", "key", .not_subscriber).call; +pub const zrandmember = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrandmember", "ZRANDMEMBER", "key", .not_subscriber).call; pub const zrange = compile.@"(...strings: string[])"("zrange", "ZRANGE", .not_subscriber).call; pub const zrevrange = compile.@"(...strings: string[])"("zrevrange", "ZREVRANGE", .not_subscriber).call; pub const zrangebyscore = compile.@"(...strings: string[])"("zrangebyscore", "ZRANGEBYSCORE", .not_subscriber).call; pub const zrevrangebyscore = compile.@"(...strings: string[])"("zrevrangebyscore", "ZREVRANGEBYSCORE", .not_subscriber).call; -pub const zrangebylex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zrangebylex", "ZRANGEBYLEX", "key", "min", "max", .not_subscriber).call; -pub const zrevrangebylex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zrevrangebylex", "ZREVRANGEBYLEX", "key", "max", "min", .not_subscriber).call; +pub const zrangebylex = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrangebylex", "ZRANGEBYLEX", "key", .not_subscriber).call; +pub const zrevrangebylex = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrevrangebylex", "ZREVRANGEBYLEX", "key", .not_subscriber).call; pub const append = compile.@"(key: RedisKey, value: RedisValue)"("append", "APPEND", "key", "value", .not_subscriber).call; pub const getset = compile.@"(key: RedisKey, value: RedisValue)"("getset", "GETSET", "key", "value", .not_subscriber).call; pub const hget = compile.@"(key: RedisKey, value: RedisValue)"("hget", "HGET", "key", "field", .not_subscriber).call; @@ -848,16 +930,44 @@ pub const msetnx = compile.@"(...strings: string[])"("msetnx", "MSETNX", .not_su pub const script = compile.@"(...strings: string[])"("script", "SCRIPT", .not_subscriber).call; pub const select = compile.@"(...strings: string[])"("select", "SELECT", .not_subscriber).call; pub const spublish = compile.@"(key: RedisKey, value: RedisValue)"("spublish", "SPUBLISH", "channel", "message", .not_subscriber).call; -pub const smove = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("smove", "SMOVE", "source", "destination", "member", .not_subscriber).call; +pub fn smove(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + try requireNotSubscriber(this, @src().fn_name); + + const source = (try fromJS(globalObject, callframe.argument(0))) orelse { + return globalObject.throwInvalidArgumentType("smove", "source", "string or buffer"); + }; + defer source.deinit(); + const destination = (try fromJS(globalObject, callframe.argument(1))) orelse { + return globalObject.throwInvalidArgumentType("smove", "destination", "string or buffer"); + }; + defer destination.deinit(); + const member = (try fromJS(globalObject, callframe.argument(2))) orelse { + return globalObject.throwInvalidArgumentType("smove", "member", "string or buffer"); + }; + defer member.deinit(); + + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = "SMOVE", + .args = .{ .args = &.{ source, destination, member } }, + .meta = .{ .return_as_bool = true }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send SMOVE command", err); + }; + return promise.toJS(); +} pub const substr = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("substr", "SUBSTR", "key", "start", "end", .not_subscriber).call; pub const hstrlen = compile.@"(key: RedisKey, value: RedisValue)"("hstrlen", "HSTRLEN", "key", "field", .not_subscriber).call; -pub const zrank = compile.@"(key: RedisKey, value: RedisValue)"("zrank", "ZRANK", "key", "member", .not_subscriber).call; +pub const zrank = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrank", "ZRANK", "key", .not_subscriber).call; pub const zrangestore = compile.@"(...strings: string[])"("zrangestore", "ZRANGESTORE", .not_subscriber).call; pub const zrem = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrem", "ZREM", "key", .not_subscriber).call; pub const zremrangebylex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebylex", "ZREMRANGEBYLEX", "key", "min", "max", .not_subscriber).call; pub const zremrangebyrank = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebyrank", "ZREMRANGEBYRANK", "key", "start", "stop", .not_subscriber).call; pub const zremrangebyscore = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebyscore", "ZREMRANGEBYSCORE", "key", "min", "max", .not_subscriber).call; -pub const zrevrank = compile.@"(key: RedisKey, value: RedisValue)"("zrevrank", "ZREVRANK", "key", "member", .not_subscriber).call; +pub const zrevrank = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrevrank", "ZREVRANK", "key", .not_subscriber).call; pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE", .dont_care).call; pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE", .dont_care).call; pub const pubsub = compile.@"(...strings: string[])"("pubsub", "PUBSUB", .dont_care).call; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 4d2f1e8a2b5e..0a5d93418373 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -537,6 +537,312 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(newLength2).toBeGreaterThanOrEqual(10); }); + test("should append to string with APPEND", async () => { + const redis = ctx.redis; + const key = "append-test"; + + // Append to non-existent key (creates new key) + const len1 = await redis.append(key, "Hello"); + expect(len1).toBe(5); + + // Append to existing key + const len2 = await redis.append(key, " World"); + expect(len2).toBe(11); + + // Verify final value + const value = await redis.get(key); + expect(value).toBe("Hello World"); + }); + + test("should delete keys with DEL", async () => { + const redis = ctx.redis; + + // Set up test keys + await redis.set("del-key1", "value1"); + await redis.set("del-key2", "value2"); + await redis.set("del-key3", "value3"); + + // Delete single key + const count1 = await redis.del("del-key1"); + expect(count1).toBe(1); + + // Verify key is deleted + const value1 = await redis.get("del-key1"); + expect(value1).toBeNull(); + + // Delete multiple keys + const count2 = await redis.del("del-key2", "del-key3"); + expect(count2).toBe(2); + + // Delete non-existent key + const count3 = await redis.del("nonexistent"); + expect(count3).toBe(0); + }); + + test("should serialize key with DUMP", async () => { + const redis = ctx.redis; + const key = "dump-test"; + + // Set a value + await redis.set(key, "test-value"); + + // Dump the key + const serialized = await redis.dump(key); + expect(serialized).toBeDefined(); + expect(serialized).not.toBeNull(); + + // Dump non-existent key + const empty = await redis.dump("nonexistent"); + expect(empty).toBeNull(); + }); + + test("should get value as Buffer with getBuffer", async () => { + const redis = ctx.redis; + const key = "getbuffer-test"; + + // Set a value + await redis.set(key, "test-value"); + + // Get as Buffer + const buffer = await redis.getBuffer(key); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer?.toString()).toBe("test-value"); + + // Non-existent key + const empty = await redis.getBuffer("nonexistent"); + expect(empty).toBeNull(); + }); + + test("should get and delete with GETDEL", async () => { + const redis = ctx.redis; + const key = "getdel-test"; + + // Set a value + await redis.set(key, "test-value"); + + // Get and delete + const value = await redis.getdel(key); + expect(value).toBe("test-value"); + + // Verify key is deleted + const deleted = await redis.get(key); + expect(deleted).toBeNull(); + + // Non-existent key + const empty = await redis.getdel("nonexistent"); + expect(empty).toBeNull(); + }); + + test("should get and set expiration with GETEX", async () => { + const redis = ctx.redis; + const key = "getex-test"; + + // Set a value + await redis.set(key, "test-value"); + + // Get with EX (seconds) + const value1 = await redis.getex(key, "EX", 60); + expect(value1).toBe("test-value"); + const ttl1 = await redis.ttl(key); + expect(ttl1).toBeGreaterThan(0); + expect(ttl1).toBeLessThanOrEqual(60); + + // Get with PX (milliseconds) + const value2 = await redis.getex(key, "PX", 5000); + expect(value2).toBe("test-value"); + const pttl = await redis.pttl(key); + expect(pttl).toBeGreaterThan(0); + expect(pttl).toBeLessThanOrEqual(5000); + + // Non-existent key + const empty = await redis.getex("nonexistent", "EX", 60); + expect(empty).toBeNull(); + }); + + test("should get old value and set new with GETSET", async () => { + const redis = ctx.redis; + const key = "getset-test"; + + // GETSET on non-existent key returns null + const old1 = await redis.getset(key, "value1"); + expect(old1).toBeNull(); + + // GETSET on existing key returns old value + const old2 = await redis.getset(key, "value2"); + expect(old2).toBe("value1"); + + // Verify new value is set + const current = await redis.get(key); + expect(current).toBe("value2"); + }); + + test("should get string length with STRLEN", async () => { + const redis = ctx.redis; + const key = "strlen-test"; + + // Non-existent key returns 0 + const len1 = await redis.strlen(key); + expect(len1).toBe(0); + + // Set values and check lengths + await redis.set(key, "Hello"); + const len2 = await redis.strlen(key); + expect(len2).toBe(5); + + await redis.set(key, "Hello World"); + const len3 = await redis.strlen(key); + expect(len3).toBe(11); + }); + + test("should get substring with SUBSTR", async () => { + const redis = ctx.redis; + const key = "substr-test"; + + await redis.set(key, "Hello World"); + + // SUBSTR is deprecated alias for GETRANGE + const result = await redis.substr(key, 0, 4); + expect(result).toBe("Hello"); + }); + + test("should get expiration time with EXPIRETIME", async () => { + const redis = ctx.redis; + const key = "expiretime-test"; + + await redis.set(key, "value"); + + // Set expiration + const futureTs = Math.floor(Date.now() / 1000) + 60; + await redis.expireat(key, futureTs); + + // Get expiration time + const expireTime = await redis.expiretime(key); + expect(expireTime).toBeGreaterThan(0); + expect(expireTime).toBeLessThanOrEqual(futureTs); + + // Key with no expiration returns -1 + const key2 = "no-expire"; + await redis.set(key2, "value"); + const noExpire = await redis.expiretime(key2); + expect(noExpire).toBe(-1); + + // Non-existent key returns -2 + const nonExist = await redis.expiretime("nonexistent"); + expect(nonExist).toBe(-2); + }); + + test("should get expiration time in ms with PEXPIRETIME", async () => { + const redis = ctx.redis; + const key = "pexpiretime-test"; + + await redis.set(key, "value"); + + // Set expiration + const futureTs = Date.now() + 5000; + await redis.pexpireat(key, futureTs); + + // Get expiration time + const pexpireTime = await redis.pexpiretime(key); + expect(pexpireTime).toBeGreaterThan(0); + expect(pexpireTime).toBeLessThanOrEqual(futureTs); + + // Key with no expiration returns -1 + const key2 = "no-expire"; + await redis.set(key2, "value"); + const noExpire = await redis.pexpiretime(key2); + expect(noExpire).toBe(-1); + + // Non-existent key returns -2 + const nonExist = await redis.pexpiretime("nonexistent"); + expect(nonExist).toBe(-2); + }); + + test("should remove expiration with PERSIST", async () => { + const redis = ctx.redis; + const key = "persist-test"; + + await redis.set(key, "value"); + await redis.expire(key, 60); + + // Verify expiration is set + const ttlBefore = await redis.ttl(key); + expect(ttlBefore).toBeGreaterThan(0); + + // Remove expiration + const result = await redis.persist(key); + expect(result).toBe(1); + + // Verify no expiration + const ttlAfter = await redis.ttl(key); + expect(ttlAfter).toBe(-1); + + // Persist on key without expiration returns 0 + const result2 = await redis.persist(key); + expect(result2).toBe(0); + + // Persist on non-existent key returns 0 + const result3 = await redis.persist("nonexistent"); + expect(result3).toBe(0); + }); + + test("should get multiple values with MGET", async () => { + const redis = ctx.redis; + + // Set multiple keys + await redis.set("mget-key1", "value1"); + await redis.set("mget-key2", "value2"); + await redis.set("mget-key3", "value3"); + + // Get multiple keys + const values = await redis.mget("mget-key1", "mget-key2", "mget-key3"); + expect(values).toEqual(["value1", "value2", "value3"]); + + // Mix of existing and non-existing keys + const mixed = await redis.mget("mget-key1", "nonexistent", "mget-key2"); + expect(mixed).toEqual(["value1", null, "value2"]); + + // All non-existent + const allNull = await redis.mget("none1", "none2", "none3"); + expect(allNull).toEqual([null, null, null]); + }); + + test("should set only if not exists with SETNX", async () => { + const redis = ctx.redis; + const key = "setnx-test"; + + // First SETNX succeeds + const result1 = await redis.setnx(key, "value1"); + expect(result1).toBe(1); + + const value1 = await redis.get(key); + expect(value1).toBe("value1"); + + // Second SETNX fails + const result2 = await redis.setnx(key, "value2"); + expect(result2).toBe(0); + + // Value unchanged + const value2 = await redis.get(key); + expect(value2).toBe("value1"); + }); + + test("should add to HyperLogLog with PFADD", async () => { + const redis = ctx.redis; + const key = "pfadd-test"; + + // Add first element + const result1 = await redis.pfadd(key, "element1"); + expect(result1).toBe(1); + + // Add different element + const result2 = await redis.pfadd(key, "element2"); + expect(result2).toBe(1); + + // Add same element (HLL not modified) + const result3 = await redis.pfadd(key, "element1"); + expect(result3).toBe(0); + }); + test("should implement TTL command correctly for different cases", async () => { const redis = ctx.redis; // 1. Key with expiration @@ -768,18 +1074,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { await redis.getrange({} as any, 0, 5); - }).toThrowErrorMatchingInlineSnapshot( - `"Expected additional arguments to be a string or buffer for 'getrange'."`, - ); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'getrange'."`); }); test("should reject invalid key in SETRANGE", async () => { const redis = ctx.redis; expect(async () => { await redis.setrange(undefined as any, 0, "value"); - }).toThrowErrorMatchingInlineSnapshot( - `"Expected additional arguments to be a string or buffer for 'setrange'."`, - ); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'setrange'."`); }); test("should reject invalid key in INCRBY", async () => { @@ -807,21 +1109,21 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { await redis.setbit({} as any, 0, 1); - }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'setbit'."`); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'setbit'."`); }); test("should reject invalid key in SETEX", async () => { const redis = ctx.redis; expect(async () => { await redis.setex(null as any, 10, "value"); - }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'setex'."`); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'setex'."`); }); test("should reject invalid key in PSETEX", async () => { const redis = ctx.redis; expect(async () => { await redis.psetex([] as any, 1000, "value"); - }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'psetex'."`); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'psetex'."`); }); test("should reject invalid key in UNLINK", async () => { @@ -874,159 +1176,509 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); }); - describe("List Operations", () => { - test("should get range of elements with LRANGE", async () => { + describe("String commands", () => { + test("should append value to key with APPEND", async () => { const redis = ctx.redis; - const key = "lrange-test"; - - // Create a list with multiple elements - await redis.lpush(key, "three"); - await redis.lpush(key, "two"); - await redis.lpush(key, "one"); - - // Get full list - const fullList = await redis.lrange(key, 0, -1); - expect(fullList).toEqual(["one", "two", "three"]); - - // Get first two elements - const firstTwo = await redis.lrange(key, 0, 1); - expect(firstTwo).toEqual(["one", "two"]); + const key = "append-test"; - // Get last two elements using negative indexes - const lastTwo = await redis.lrange(key, -2, -1); - expect(lastTwo).toEqual(["two", "three"]); + // Append to non-existent key (should create it) + const length1 = await redis.append(key, "Hello"); + expect(length1).toBe(5); - // Get middle element - const middle = await redis.lrange(key, 1, 1); - expect(middle).toEqual(["two"]); + const value1 = await redis.get(key); + expect(value1).toBe("Hello"); - // Out of range should return empty array - const outOfRange = await redis.lrange(key, 10, 20); - expect(outOfRange).toEqual([]); + // Append to existing key + const length2 = await redis.append(key, " World"); + expect(length2).toBe(11); - // Non-existent key should return empty array - const nonExistent = await redis.lrange("nonexistent-list", 0, -1); - expect(nonExistent).toEqual([]); + const value2 = await redis.get(key); + expect(value2).toBe("Hello World"); }); - test("should get element at index with LINDEX", async () => { + test("should delete one or more keys with DEL", async () => { const redis = ctx.redis; - const key = "lindex-test"; - // Create a list - await redis.lpush(key, "three"); - await redis.lpush(key, "two"); - await redis.lpush(key, "one"); + // Set multiple keys + await redis.set("del-test-1", "value1"); + await redis.set("del-test-2", "value2"); + await redis.set("del-test-3", "value3"); - // Get element at positive index - const first = await redis.lindex(key, 0); - expect(first).toBe("one"); + // Delete single key + const result1 = await redis.del("del-test-1"); + expect(result1).toBe(1); - const second = await redis.lindex(key, 1); - expect(second).toBe("two"); + // Verify key was deleted + expect(await redis.get("del-test-1")).toBeNull(); - const third = await redis.lindex(key, 2); - expect(third).toBe("three"); + // Delete multiple keys + const result2 = await redis.del("del-test-2", "del-test-3"); + expect(result2).toBe(2); - // Get element at negative index (counting from end) - const last = await redis.lindex(key, -1); - expect(last).toBe("three"); + // Verify keys were deleted + expect(await redis.get("del-test-2")).toBeNull(); + expect(await redis.get("del-test-3")).toBeNull(); - const secondLast = await redis.lindex(key, -2); - expect(secondLast).toBe("two"); + // Delete non-existent key + const result3 = await redis.del("del-test-nonexistent"); + expect(result3).toBe(0); + }); - // Out of range should return null - const outOfRange = await redis.lindex(key, 10); - expect(outOfRange).toBeNull(); + test("should serialize key with DUMP", async () => { + const redis = ctx.redis; + const key = "dump-test"; - const outOfRangeNeg = await redis.lindex(key, -10); - expect(outOfRangeNeg).toBeNull(); + // Set a value + await redis.set(key, "Hello World"); - // Non-existent key should return null - const nonExistent = await redis.lindex("nonexistent-list", 0); + // Dump the key + const serialized = await redis.dump(key); + expect(serialized).toBeDefined(); + expect(serialized).not.toBeNull(); + + // DUMP returns binary data, so it should be a Buffer or string + expect(typeof serialized === "string" || Buffer.isBuffer(serialized)).toBe(true); + + // Dump non-existent key should return null + const nonExistent = await redis.dump("dump-test-nonexistent"); expect(nonExistent).toBeNull(); }); - test("should set element at index with LSET", async () => { + test("should get value as Buffer with getBuffer", async () => { const redis = ctx.redis; - const key = "lset-test"; + const key = "getbuffer-test"; - // Create a list - await redis.lpush(key, "three"); - await redis.lpush(key, "two"); - await redis.lpush(key, "one"); + // Set a value + await redis.set(key, "Hello Buffer"); - // Set element at positive index - const result1 = await redis.lset(key, 0, "zero"); - expect(result1).toBe("OK"); + // Get as Buffer + const buffer = await redis.getBuffer(key); + expect(buffer).toBeDefined(); + expect(buffer).not.toBeNull(); + expect(Buffer.isBuffer(buffer)).toBe(true); + expect(buffer!.toString()).toBe("Hello Buffer"); - // Verify the change - const first = await redis.lindex(key, 0); - expect(first).toBe("zero"); + // Get non-existent key + const nonExistent = await redis.getBuffer("getbuffer-nonexistent"); + expect(nonExistent).toBeNull(); + }); - // Set element at negative index - const result2 = await redis.lset(key, -1, "last"); - expect(result2).toBe("OK"); + test("should get and delete key with GETDEL", async () => { + const redis = ctx.redis; + const key = "getdel-test"; - // Verify the change - const last = await redis.lindex(key, -1); - expect(last).toBe("last"); + // Set a value + await redis.set(key, "Delete me"); - // Check full list - const fullList = await redis.lrange(key, 0, -1); - expect(fullList).toEqual(["zero", "two", "last"]); + // Get and delete + const value = await redis.getdel(key); + expect(value).toBe("Delete me"); + + // Verify key was deleted + const check = await redis.get(key); + expect(check).toBeNull(); + + // GETDEL on non-existent key + const nonExistent = await redis.getdel("getdel-nonexistent"); + expect(nonExistent).toBeNull(); }); - test("should handle LSET errors", async () => { + test("should get key with expiration using GETEX with EX", async () => { const redis = ctx.redis; + const key = "getex-ex-test"; - await redis.lpush("lset-error-test", "value"); + // Set a value + await redis.set(key, "Expire me"); - expect(async () => { - await redis.lset("lset-error-test", 10, "newvalue"); - }).toThrow(/index out of range/i); + // Get with EX option (seconds) + const value = await redis.getex(key, "EX", 10); + expect(value).toBe("Expire me"); - // Non-existent key should throw an error - expect(async () => { - await redis.lset("nonexistent-list", 0, "value"); - }).toThrow(/no such key/i); + // Verify key still exists + const check = await redis.get(key); + expect(check).toBe("Expire me"); - // Wrong type (not a list) should throw an error - await redis.set("string-key", "value"); - expect(async () => { - await redis.lset("string-key", 0, "value"); - }).toThrow(/wrong.*type|WRONGTYPE/i); + // Verify TTL was set + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(10); }); - test("should handle LRANGE with various ranges", async () => { + test("should get key with expiration using GETEX with PX", async () => { const redis = ctx.redis; - const key = "lrange-advanced"; - - // Create a longer list - for (let i = 5; i >= 1; i--) { - await redis.lpush(key, String(i)); - } - - // Verify the list: [1, 2, 3, 4, 5] - const fullList = await redis.lrange(key, 0, -1); - expect(fullList).toEqual(["1", "2", "3", "4", "5"]); + const key = "getex-px-test"; - // Test with stop less than start (should return empty) - const invalid = await redis.lrange(key, 3, 1); - expect(invalid).toEqual([]); + // Set a value + await redis.set(key, "Expire me"); - // Test with negative start and positive stop - const mixed = await redis.lrange(key, -3, 4); - expect(mixed).toEqual(["3", "4", "5"]); + // Get with PX option (milliseconds) + const value = await redis.getex(key, "PX", 5000); + expect(value).toBe("Expire me"); - // Test with both negative - const bothNeg = await redis.lrange(key, -4, -2); - expect(bothNeg).toEqual(["2", "3", "4"]); + // Verify TTL was set (in seconds) + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(5); }); - test("should handle LINDEX and LSET with numbers", async () => { + test("should get key with expiration using GETEX with EXAT", async () => { const redis = ctx.redis; - const key = "list-numbers"; + const key = "getex-exat-test"; + + // Set a value + await redis.set(key, "Expire at timestamp"); + + // Get with EXAT option (unix timestamp in seconds) + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; + const value = await redis.getex(key, "EXAT", futureTimestamp); + expect(value).toBe("Expire at timestamp"); + + // Verify TTL was set + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(60); + }); + + test("should get key with expiration using GETEX with PXAT", async () => { + const redis = ctx.redis; + const key = "getex-pxat-test"; + + // Set a value + await redis.set(key, "Expire at timestamp"); + + // Get with PXAT option (unix timestamp in milliseconds) + const futureTimestamp = Date.now() + 60000; + const value = await redis.getex(key, "PXAT", futureTimestamp); + expect(value).toBe("Expire at timestamp"); + + // Verify TTL was set + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(60); + }); + + test("should persist key expiration using GETEX with PERSIST", async () => { + const redis = ctx.redis; + const key = "getex-persist-test"; + + // Set a value with expiration + await redis.set(key, "Remove expiration", "EX", 100); + + // Verify it has expiration + const ttlBefore = await redis.ttl(key); + expect(ttlBefore).toBeGreaterThan(0); + + // Get with PERSIST option + const value = await redis.getex(key, "PERSIST"); + expect(value).toBe("Remove expiration"); + + // Verify expiration was removed + const ttlAfter = await redis.ttl(key); + expect(ttlAfter).toBe(-1); // -1 means no expiration + }); + + test("should get non-existent key with GETEX", async () => { + const redis = ctx.redis; + + const value = await redis.getex("getex-nonexistent", "EX", 10); + expect(value).toBeNull(); + }); + + test("should get and set in one operation with GETSET", async () => { + const redis = ctx.redis; + const key = "getset-test"; + + // GETSET on non-existent key returns null + const oldValue1 = await redis.getset(key, "new value"); + expect(oldValue1).toBeNull(); + + // Verify new value was set + const check1 = await redis.get(key); + expect(check1).toBe("new value"); + + // GETSET on existing key returns old value + const oldValue2 = await redis.getset(key, "newer value"); + expect(oldValue2).toBe("new value"); + + // Verify new value was set + const check2 = await redis.get(key); + expect(check2).toBe("newer value"); + }); + + test("should get string length with STRLEN", async () => { + const redis = ctx.redis; + const key = "strlen-test"; + + // STRLEN on non-existent key + const length1 = await redis.strlen("strlen-nonexistent"); + expect(length1).toBe(0); + + // Set a value and get its length + await redis.set(key, "Hello World"); + const length2 = await redis.strlen(key); + expect(length2).toBe(11); + + // Test with different value + await redis.set(key, "Hi"); + const length3 = await redis.strlen(key); + expect(length3).toBe(2); + }); + }); + + describe("List Operations", () => { + test("should get list length with LLEN", async () => { + const redis = ctx.redis; + const key = "llen-test"; + + // Empty list should return 0 + const len1 = await redis.llen(key); + expect(len1).toBe(0); + + // Add elements + await redis.lpush(key, "one", "two", "three"); + + // Get length + const len2 = await redis.llen(key); + expect(len2).toBe(3); + + // Pop one element + await redis.lpop(key); + const len3 = await redis.llen(key); + expect(len3).toBe(2); + }); + + test("should pop left with LPOP", async () => { + const redis = ctx.redis; + const key = "lpop-test"; + + // Add elements + await redis.lpush(key, "three", "two", "one"); + + // Pop one element + const elem1 = await redis.lpop(key); + expect(elem1).toBe("one"); + + // Pop with count + const elem2 = await redis.lpop(key, 2); + expect(elem2).toEqual(["two", "three"]); + + // Pop from empty list + const empty = await redis.lpop(key); + expect(empty).toBeNull(); + }); + + test("should push to existing list with LPUSHX", async () => { + const redis = ctx.redis; + const key = "lpushx-test"; + + // LPUSHX on non-existent key should return 0 + const len1 = await redis.lpushx(key, "value"); + expect(len1).toBe(0); + + // Create the list first + await redis.lpush(key, "one"); + + // Now LPUSHX should work + const len2 = await redis.lpushx(key, "two"); + expect(len2).toBe(2); + + // Verify order + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["two", "one"]); + }); + + test("should pop right with RPOP", async () => { + const redis = ctx.redis; + const key = "rpop-test"; + + // Add elements + await redis.rpush(key, "one", "two", "three"); + + // Pop one element + const elem1 = await redis.rpop(key); + expect(elem1).toBe("three"); + + // Pop with count + const elem2 = await redis.rpop(key, 2); + expect(elem2).toEqual(["two", "one"]); + + // Pop from empty list + const empty = await redis.rpop(key); + expect(empty).toBeNull(); + }); + + test("should push to existing list with RPUSHX", async () => { + const redis = ctx.redis; + const key = "rpushx-test"; + + // RPUSHX on non-existent key should return 0 + const len1 = await redis.rpushx(key, "value"); + expect(len1).toBe(0); + + // Create the list first + await redis.rpush(key, "one"); + + // Now RPUSHX should work + const len2 = await redis.rpushx(key, "two"); + expect(len2).toBe(2); + + // Verify order + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["one", "two"]); + }); + + test("should get range of elements with LRANGE", async () => { + const redis = ctx.redis; + const key = "lrange-test"; + + // Create a list with multiple elements + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Get full list + const fullList = await redis.lrange(key, 0, -1); + expect(fullList).toEqual(["one", "two", "three"]); + + // Get first two elements + const firstTwo = await redis.lrange(key, 0, 1); + expect(firstTwo).toEqual(["one", "two"]); + + // Get last two elements using negative indexes + const lastTwo = await redis.lrange(key, -2, -1); + expect(lastTwo).toEqual(["two", "three"]); + + // Get middle element + const middle = await redis.lrange(key, 1, 1); + expect(middle).toEqual(["two"]); + + // Out of range should return empty array + const outOfRange = await redis.lrange(key, 10, 20); + expect(outOfRange).toEqual([]); + + // Non-existent key should return empty array + const nonExistent = await redis.lrange("nonexistent-list", 0, -1); + expect(nonExistent).toEqual([]); + }); + + test("should get element at index with LINDEX", async () => { + const redis = ctx.redis; + const key = "lindex-test"; + + // Create a list + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Get element at positive index + const first = await redis.lindex(key, 0); + expect(first).toBe("one"); + + const second = await redis.lindex(key, 1); + expect(second).toBe("two"); + + const third = await redis.lindex(key, 2); + expect(third).toBe("three"); + + // Get element at negative index (counting from end) + const last = await redis.lindex(key, -1); + expect(last).toBe("three"); + + const secondLast = await redis.lindex(key, -2); + expect(secondLast).toBe("two"); + + // Out of range should return null + const outOfRange = await redis.lindex(key, 10); + expect(outOfRange).toBeNull(); + + const outOfRangeNeg = await redis.lindex(key, -10); + expect(outOfRangeNeg).toBeNull(); + + // Non-existent key should return null + const nonExistent = await redis.lindex("nonexistent-list", 0); + expect(nonExistent).toBeNull(); + }); + + test("should set element at index with LSET", async () => { + const redis = ctx.redis; + const key = "lset-test"; + + // Create a list + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Set element at positive index + const result1 = await redis.lset(key, 0, "zero"); + expect(result1).toBe("OK"); + + // Verify the change + const first = await redis.lindex(key, 0); + expect(first).toBe("zero"); + + // Set element at negative index + const result2 = await redis.lset(key, -1, "last"); + expect(result2).toBe("OK"); + + // Verify the change + const last = await redis.lindex(key, -1); + expect(last).toBe("last"); + + // Check full list + const fullList = await redis.lrange(key, 0, -1); + expect(fullList).toEqual(["zero", "two", "last"]); + }); + + test("should handle LSET errors", async () => { + const redis = ctx.redis; + + await redis.lpush("lset-error-test", "value"); + + expect(async () => { + await redis.lset("lset-error-test", 10, "newvalue"); + }).toThrow(/index out of range/i); + + // Non-existent key should throw an error + expect(async () => { + await redis.lset("nonexistent-list", 0, "value"); + }).toThrow(/no such key/i); + + // Wrong type (not a list) should throw an error + await redis.set("string-key", "value"); + expect(async () => { + await redis.lset("string-key", 0, "value"); + }).toThrow(/wrong.*type|WRONGTYPE/i); + }); + + test("should handle LRANGE with various ranges", async () => { + const redis = ctx.redis; + const key = "lrange-advanced"; + + // Create a longer list + for (let i = 5; i >= 1; i--) { + await redis.lpush(key, String(i)); + } + + // Verify the list: [1, 2, 3, 4, 5] + const fullList = await redis.lrange(key, 0, -1); + expect(fullList).toEqual(["1", "2", "3", "4", "5"]); + + // Test with stop less than start (should return empty) + const invalid = await redis.lrange(key, 3, 1); + expect(invalid).toEqual([]); + + // Test with negative start and positive stop + const mixed = await redis.lrange(key, -3, 4); + expect(mixed).toEqual(["3", "4", "5"]); + + // Test with both negative + const bothNeg = await redis.lrange(key, -4, -2); + expect(bothNeg).toEqual(["2", "3", "4"]); + }); + + test("should handle LINDEX and LSET with numbers", async () => { + const redis = ctx.redis; + const key = "list-numbers"; // Push numeric strings await redis.lpush(key, "100"); @@ -1756,26 +2408,382 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const result = await redis.brpoplpush("empty-source", "dest", 0.1); expect(result).toBeNull(); }); - }); - describe("Set Operations", () => { - test("should return intersection of two sets with SINTER", async () => { + test("should get list length with LLEN", async () => { const redis = ctx.redis; - const key1 = "set1"; - const key2 = "set2"; + const key = "llen-test"; - // Create two sets with some overlapping members - await redis.sadd(key1, "a"); - await redis.sadd(key1, "b"); - await redis.sadd(key1, "c"); - await redis.sadd(key2, "b"); - await redis.sadd(key2, "c"); - await redis.sadd(key2, "d"); + // Empty list should return 0 + const empty = await redis.llen(key); + expect(empty).toBe(0); - // Get intersection - const result = await redis.sinter(key1, key2); - expect(result.sort()).toEqual(["b", "c"]); - }); + // Add elements + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Should return correct length + const length = await redis.llen(key); + expect(length).toBe(3); + + // Non-existent key should return 0 + const nonExistent = await redis.llen("nonexistent-list"); + expect(nonExistent).toBe(0); + }); + + test("should pop element from head with LPOP", async () => { + const redis = ctx.redis; + const key = "lpop-test"; + + // Push elements + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Pop from head (left side) + const first = await redis.lpop(key); + expect(first).toBe("one"); + + // Pop again + const second = await redis.lpop(key); + expect(second).toBe("two"); + + // Verify remaining element + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual(["three"]); + + // Pop last element + const last = await redis.lpop(key); + expect(last).toBe("three"); + + // Pop from empty list should return null + const empty = await redis.lpop(key); + expect(empty).toBeNull(); + + // Non-existent key should return null + const nonExistent = await redis.lpop("nonexistent-list"); + expect(nonExistent).toBeNull(); + }); + + test("should pop element from tail with RPOP", async () => { + const redis = ctx.redis; + const key = "rpop-test"; + + // Push elements + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + // Pop from tail (right side) + const first = await redis.rpop(key); + expect(first).toBe("three"); + + // Pop again + const second = await redis.rpop(key); + expect(second).toBe("two"); + + // Verify remaining element + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual(["one"]); + + // Pop last element + const last = await redis.rpop(key); + expect(last).toBe("one"); + + // Pop from empty list should return null + const empty = await redis.rpop(key); + expect(empty).toBeNull(); + + // Non-existent key should return null + const nonExistent = await redis.rpop("nonexistent-list"); + expect(nonExistent).toBeNull(); + }); + + test("should push element to head only if key exists with LPUSHX", async () => { + const redis = ctx.redis; + const key = "lpushx-test"; + + // LPUSHX on non-existent key should return 0 and not create the key + const nonExistent = await redis.lpushx(key, "value"); + expect(nonExistent).toBe(0); + + // Verify key was not created + const exists = await redis.exists(key); + expect(exists).toBe(false); + + // Create the list first + await redis.lpush(key, "initial"); + + // LPUSHX should now work + const result = await redis.lpushx(key, "new"); + expect(result).toBe(2); // New length + + // Verify the list + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["new", "initial"]); + + // Push multiple elements + const result2 = await redis.lpushx(key, "newer"); + expect(result2).toBe(3); + + const finalList = await redis.lrange(key, 0, -1); + expect(finalList).toEqual(["newer", "new", "initial"]); + }); + + test("should push element to tail only if key exists with RPUSHX", async () => { + const redis = ctx.redis; + const key = "rpushx-test"; + + // RPUSHX on non-existent key should return 0 and not create the key + const nonExistent = await redis.rpushx(key, "value"); + expect(nonExistent).toBe(0); + + // Verify key was not created + const exists = await redis.exists(key); + expect(exists).toBe(false); + + // Create the list first + await redis.lpush(key, "initial"); + + // RPUSHX should now work + const result = await redis.rpushx(key, "new"); + expect(result).toBe(2); // New length + + // Verify the list + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["initial", "new"]); + + // Push multiple elements + const result2 = await redis.rpushx(key, "newer"); + expect(result2).toBe(3); + + const finalList = await redis.lrange(key, 0, -1); + expect(finalList).toEqual(["initial", "new", "newer"]); + }); + }); + + describe("Set Operations", () => { + test("should get set cardinality with SCARD", async () => { + const redis = ctx.redis; + const key = "scard-test"; + + // Empty set + const count1 = await redis.scard(key); + expect(count1).toBe(0); + + // Add members + await redis.sadd(key, "one", "two", "three"); + const count2 = await redis.scard(key); + expect(count2).toBe(3); + + // Adding duplicate doesn't change count + await redis.sadd(key, "two"); + const count3 = await redis.scard(key); + expect(count3).toBe(3); + }); + + test("should get set difference with SDIFF", async () => { + const redis = ctx.redis; + const key1 = "sdiff-test1"; + const key2 = "sdiff-test2"; + + await redis.sadd(key1, "a", "b", "c", "d"); + await redis.sadd(key2, "c", "d", "e"); + + const diff = await redis.sdiff(key1, key2); + expect(diff.sort()).toEqual(["a", "b"]); + + // Non-existent key + const diff2 = await redis.sdiff(key1, "nonexistent"); + expect(diff2.sort()).toEqual(["a", "b", "c", "d"]); + }); + + test("should check set membership with SISMEMBER", async () => { + const redis = ctx.redis; + const key = "sismember-test"; + + await redis.sadd(key, "one", "two", "three"); + + const result1 = await redis.sismember(key, "one"); + expect(result1).toBe(true); + + const result2 = await redis.sismember(key, "nonexistent"); + expect(result2).toBe(false); + + const result3 = await redis.sismember("nonexistent", "member"); + expect(result3).toBe(false); + }); + + test("should move member between sets with SMOVE", async () => { + const redis = ctx.redis; + const source = "smove-source"; + const dest = "smove-dest"; + + await redis.sadd(source, "one", "two", "three"); + await redis.sadd(dest, "four"); + + // Move existing member + const result1 = await redis.smove(source, dest, "one"); + expect(result1).toBe(true); + + // Verify move + const sourceMembers = await redis.smembers(source); + expect(sourceMembers.sort()).toEqual(["three", "two"]); + + const destMembers = await redis.smembers(dest); + expect(destMembers.sort()).toEqual(["four", "one"]); + + // Move non-existent member + const result2 = await redis.smove(source, dest, "nonexistent"); + expect(result2).toBe(false); + }); + + test("should pop random member with SPOP", async () => { + const redis = ctx.redis; + const key = "spop-test"; + + await redis.sadd(key, "one", "two", "three", "four"); + + // Pop one member + const popped1 = await redis.spop(key); + expect(["one", "two", "three", "four"]).toContain(popped1); + + // Verify it was removed + const remaining1 = await redis.scard(key); + expect(remaining1).toBe(3); + + // Pop multiple members + const popped2 = await redis.spop(key, 2); + expect(Array.isArray(popped2)).toBe(true); + expect(popped2).toBeDefined(); + expect(popped2!.length).toBe(2); + + // Verify removed + const remaining2 = await redis.scard(key); + expect(remaining2).toBe(1); + + // Pop from empty set + await redis.spop(key); + const empty = await redis.spop(key); + expect(empty).toBeNull(); + }); + + test("should publish to sharded channel with SPUBLISH", async () => { + const redis = ctx.redis; + + // SPUBLISH returns number of subscribers (0 if no one listening) + const result = await redis.spublish("test-channel", "test-message"); + expect(typeof result).toBe("number"); + expect(result).toBeGreaterThanOrEqual(0); + }); + + test("should get random member with SRANDMEMBER", async () => { + const redis = ctx.redis; + const key = "srandmember-test"; + + await redis.sadd(key, "one", "two", "three"); + + // Get one random member + const member1 = await redis.srandmember(key); + expect(["one", "two", "three"]).toContain(member1); + + // Get multiple random members + const members = await redis.srandmember(key, 2); + expect(Array.isArray(members)).toBe(true); + expect(members!.length).toBeLessThanOrEqual(2); + + // Verify NOT removed (unlike SPOP) + const count = await redis.scard(key); + expect(count).toBe(3); + + // Empty set + const empty = await redis.srandmember("nonexistent"); + expect(empty).toBeNull(); + }); + + test("should remove members with SREM", async () => { + const redis = ctx.redis; + const key = "srem-test"; + + await redis.sadd(key, "one", "two", "three", "four"); + + // Remove one member + const count1 = await redis.srem(key, "one"); + expect(count1).toBe(1); + + // Remove multiple members + const count2 = await redis.srem(key, "two", "three"); + expect(count2).toBe(2); + + // Verify remaining + const remaining = await redis.smembers(key); + expect(remaining).toEqual(["four"]); + + // Remove non-existent member + const count3 = await redis.srem(key, "nonexistent"); + expect(count3).toBe(0); + }); + + test("should get set union with SUNION", async () => { + const redis = ctx.redis; + const key1 = "sunion-test1"; + const key2 = "sunion-test2"; + + await redis.sadd(key1, "a", "b", "c"); + await redis.sadd(key2, "c", "d", "e"); + + const union = await redis.sunion(key1, key2); + expect(union.sort()).toEqual(["a", "b", "c", "d", "e"]); + + // With non-existent set + const union2 = await redis.sunion(key1, "nonexistent"); + expect(union2.sort()).toEqual(["a", "b", "c"]); + }); + + test("should store set union with SUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "sunionstore-test1"; + const key2 = "sunionstore-test2"; + const dest = "sunionstore-dest"; + + await redis.sadd(key1, "a", "b", "c"); + await redis.sadd(key2, "c", "d", "e"); + + // Store union + const count = await redis.sunionstore(dest, key1, key2); + expect(count).toBe(5); + + // Verify stored + const stored = await redis.smembers(dest); + expect(stored.sort()).toEqual(["a", "b", "c", "d", "e"]); + + // Overwrite existing destination + await redis.sadd(dest, "z"); + const count2 = await redis.sunionstore(dest, key1, key2); + expect(count2).toBe(5); + + const stored2 = await redis.smembers(dest); + expect(stored2.sort()).toEqual(["a", "b", "c", "d", "e"]); + expect(stored2).not.toContain("z"); + }); + + test("should return intersection of two sets with SINTER", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + // Create two sets with some overlapping members + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + // Get intersection + const result = await redis.sinter(key1, key2); + expect(result.sort()).toEqual(["b", "c"]); + }); test("should return intersection of multiple sets with SINTER", async () => { const redis = ctx.redis; @@ -2112,91 +3120,583 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await (redis as any).sscan(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sscan' command"`); }); - }); - describe("Sorted Set Operations", () => { - test("should increment score with ZINCRBY", async () => { + test("should get cardinality of set with SCARD", async () => { const redis = ctx.redis; - const key = "zincrby-test"; + const key = "scard-test"; - // Add initial members - await redis.send("ZADD", [key, "1.0", "member1", "2.0", "member2"]); + // Empty set should have cardinality 0 + const emptyCount = await redis.scard(key); + expect(emptyCount).toBe(0); - // Increment member1's score by 2.5 - const newScore1 = await redis.zincrby(key, 2.5, "member1"); - expect(newScore1).toBe(3.5); + // Add members + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); - // Increment member2's score by -1.5 - const newScore2 = await redis.zincrby(key, -1.5, "member2"); - expect(newScore2).toBe(0.5); + // Should have 3 members + const count = await redis.scard(key); + expect(count).toBe(3); - // Increment non-existent member (should create it with the increment as score) - const newScore3 = await redis.zincrby(key, 5, "member3"); - expect(newScore3).toBe(5); + // Adding duplicate should not change cardinality + await redis.sadd(key, "a"); + const sameCount = await redis.scard(key); + expect(sameCount).toBe(3); }); - test("should count members in score range with ZCOUNT", async () => { + test("should get difference of sets with SDIFF", async () => { const redis = ctx.redis; - const key = "zcount-test"; + const key1 = "sdiff-test1"; + const key2 = "sdiff-test2"; + const key3 = "sdiff-test3"; - // Add members with scores - await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + // Set up test sets + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key1, "d"); - // Count all members - const count1 = await redis.zcount(key, "-inf", "+inf"); - expect(count1).toBe(5); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); - // Count members with score between 2 and 4 (inclusive) - const count2 = await redis.zcount(key, 2, 4); - expect(count2).toBe(3); // two, three, four + await redis.sadd(key3, "d"); - // Count with specific range - const count3 = await redis.zcount(key, 1, 3); - expect(count3).toBe(3); // one, two, three + // Difference between key1 and key2 + const diff1 = await redis.sdiff(key1, key2); + expect(diff1.sort()).toEqual(["a", "d"]); - // Count with no matches - const count4 = await redis.zcount(key, 10, 20); - expect(count4).toBe(0); + // Difference with multiple sets + const diff2 = await redis.sdiff(key1, key2, key3); + expect(diff2.sort()).toEqual(["a"]); + + // Difference with non-existent set (should return all from first set) + const diff3 = await redis.sdiff(key1, "nonexistent"); + expect(diff3.sort()).toEqual(["a", "b", "c", "d"]); + + // Empty result + const diff4 = await redis.sdiff(key2, key1); + expect(diff4).toEqual([]); }); - test("should count members in lexicographical range with ZLEXCOUNT", async () => { + test("should check if member exists in set with SISMEMBER", async () => { const redis = ctx.redis; - const key = "zlexcount-test"; + const key = "sismember-test"; - // Add members with same score (required for lex operations) - await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + // Add members + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); - // Count all members - const count1 = await redis.zlexcount(key, "-", "+"); - expect(count1).toBe(5); + // Check existing members + const exists1 = await redis.sismember(key, "a"); + expect(exists1).toBe(true); - // Count members from "banana" to "date" (inclusive) - const count2 = await redis.zlexcount(key, "[banana", "[date"); - expect(count2).toBe(3); // banana, cherry, date + const exists2 = await redis.sismember(key, "b"); + expect(exists2).toBe(true); - // Count with exclusive range - const count3 = await redis.zlexcount(key, "(banana", "(date"); - expect(count3).toBe(1); // only cherry + // Check non-existent member + const notExists = await redis.sismember(key, "z"); + expect(notExists).toBe(false); - // Count with no matches - const count4 = await redis.zlexcount(key, "[zebra", "[zoo"); - expect(count4).toBe(0); + // Check on non-existent set + const notExistsSet = await redis.sismember("nonexistent", "a"); + expect(notExistsSet).toBe(false); }); - test("should compute difference between sorted sets with ZDIFF", async () => { + test("should move member between sets with SMOVE", async () => { const redis = ctx.redis; - const key1 = "zdiff-test1"; - const key2 = "zdiff-test2"; - const key3 = "zdiff-test3"; + const source = "smove-source"; + const dest = "smove-dest"; - // Set up sorted sets - await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three", "4", "four"]); - await redis.send("ZADD", [key2, "1", "one", "2", "two"]); - await redis.send("ZADD", [key3, "3", "three"]); + // Set up source set + await redis.sadd(source, "a"); + await redis.sadd(source, "b"); + await redis.sadd(source, "c"); - // Difference between first and second set - const diff1 = await redis.zdiff(2, key1, key2); - expect(diff1).toEqual(["three", "four"]); + // Set up destination set + await redis.sadd(dest, "x"); + await redis.sadd(dest, "y"); + + // Move member from source to dest + const moved = await redis.smove(source, dest, "b"); + expect(moved).toBe(true); + + // Verify source no longer has the member + const sourceMembers = await redis.smembers(source); + expect(sourceMembers.sort()).toEqual(["a", "c"]); + + // Verify dest has the member + const destMembers = await redis.smembers(dest); + expect(destMembers.sort()).toEqual(["b", "x", "y"]); + + // Try to move non-existent member + const notMoved = await redis.smove(source, dest, "z"); + expect(notMoved).toBe(false); + + // Try to move from non-existent set + const notMoved2 = await redis.smove("nonexistent", dest, "a"); + expect(notMoved2).toBe(false); + }); + + test("should remove and return random member with SPOP", async () => { + const redis = ctx.redis; + const key = "spop-test"; + + // Add members + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); + await redis.sadd(key, "d"); + await redis.sadd(key, "e"); + + // Pop single member + const popped = await redis.spop(key); + expect(popped).toBeDefined(); + expect(["a", "b", "c", "d", "e"]).toContain(popped); + + // Verify member was removed + const remaining = await redis.scard(key); + expect(remaining).toBe(4); + + // Pop multiple members + const poppedMultiple = await redis.spop(key, 2); + expect(Array.isArray(poppedMultiple)).toBe(true); + expect(poppedMultiple!.length).toBe(2); + poppedMultiple!.forEach(member => { + expect(["a", "b", "c", "d", "e"]).toContain(member); + }); + + // Verify members were removed + const remainingAfter = await redis.scard(key); + expect(remainingAfter).toBe(2); + + // Pop from empty set + await redis.spop(key, 10); // Pop all remaining + const emptyPop = await redis.spop(key); + expect(emptyPop).toBeNull(); + }); + + test("should publish to sharded channel with SPUBLISH", async () => { + const redis = ctx.redis; + const channel = "spublish-channel"; + + // SPUBLISH returns number of subscribers + // With no subscribers, should return 0 + const count = await redis.spublish(channel, "test message"); + expect(typeof count).toBe("number"); + expect(count).toBe(0); + }); + + test("should get random member without removing with SRANDMEMBER", async () => { + const redis = ctx.redis; + const key = "srandmember-test"; + + // Add members + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); + await redis.sadd(key, "d"); + await redis.sadd(key, "e"); + + // Get single random member + const random = await redis.srandmember(key); + expect(random).toBeDefined(); + expect(["a", "b", "c", "d", "e"]).toContain(random); + + // Verify member was NOT removed + const count = await redis.scard(key); + expect(count).toBe(5); + + // Get multiple random members + const randomMultiple = await redis.srandmember(key, 3); + expect(Array.isArray(randomMultiple)).toBe(true); + expect(randomMultiple!.length).toBe(3); + randomMultiple!.forEach(member => { + expect(["a", "b", "c", "d", "e"]).toContain(member); + }); + + // Verify members were NOT removed + const countAfter = await redis.scard(key); + expect(countAfter).toBe(5); + + // Get with count larger than set size (no duplicates) + const tooMany = await redis.srandmember(key, 10); + expect(Array.isArray(tooMany)).toBe(true); + expect(tooMany!.length).toBe(5); + + // Get with negative count (allows duplicates) + const withDuplicates = await redis.srandmember(key, -10); + expect(withDuplicates!.length).toBe(10); + withDuplicates!.forEach(member => { + expect(["a", "b", "c", "d", "e"]).toContain(member); + }); + + // Get from empty set + const emptyRandom = await redis.srandmember("nonexistent"); + expect(emptyRandom).toBeNull(); + }); + + test("should remove members from set with SREM", async () => { + const redis = ctx.redis; + const key = "srem-test"; + + // Add members + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); + await redis.sadd(key, "d"); + await redis.sadd(key, "e"); + + // Remove single member + const removed1 = await redis.srem(key, "a"); + expect(removed1).toBe(1); + + // Verify member was removed + const members1 = await redis.smembers(key); + expect(members1.sort()).toEqual(["b", "c", "d", "e"]); + + // Remove multiple members + const removed2 = await redis.srem(key, "b", "c", "z"); + expect(removed2).toBe(2); // "z" doesn't exist, so only 2 removed + + // Verify members were removed + const members2 = await redis.smembers(key); + expect(members2.sort()).toEqual(["d", "e"]); + + // Remove non-existent member + const removed3 = await redis.srem(key, "nonexistent"); + expect(removed3).toBe(0); + + // Remove from non-existent set + const removed4 = await redis.srem("nonexistent", "a"); + expect(removed4).toBe(0); + }); + + test("should get union of sets with SUNION", async () => { + const redis = ctx.redis; + const key1 = "sunion-test1"; + const key2 = "sunion-test2"; + const key3 = "sunion-test3"; + + // Set up test sets + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + await redis.sadd(key2, "e"); + + await redis.sadd(key3, "e"); + await redis.sadd(key3, "f"); + await redis.sadd(key3, "g"); + + // Union of two sets + const union1 = await redis.sunion(key1, key2); + expect(union1.sort()).toEqual(["a", "b", "c", "d", "e"]); + + // Union of three sets + const union2 = await redis.sunion(key1, key2, key3); + expect(union2.sort()).toEqual(["a", "b", "c", "d", "e", "f", "g"]); + + // Union with non-existent set + const union3 = await redis.sunion(key1, "nonexistent"); + expect(union3.sort()).toEqual(["a", "b", "c"]); + + // Union with empty set + const union4 = await redis.sunion("nonexistent1", "nonexistent2"); + expect(union4).toEqual([]); + }); + + test("should store union of sets with SUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "sunionstore-test1"; + const key2 = "sunionstore-test2"; + const key3 = "sunionstore-test3"; + const dest = "sunionstore-dest"; + + // Set up test sets + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + await redis.sadd(key2, "e"); + + await redis.sadd(key3, "e"); + await redis.sadd(key3, "f"); + + // Store union of two sets + const count1 = await redis.sunionstore(dest, key1, key2); + expect(count1).toBe(5); + + // Verify destination has the union + const members1 = await redis.smembers(dest); + expect(members1.sort()).toEqual(["a", "b", "c", "d", "e"]); + + // Store union of three sets (should overwrite destination) + const count2 = await redis.sunionstore(dest, key1, key2, key3); + expect(count2).toBe(6); + + // Verify destination was overwritten + const members2 = await redis.smembers(dest); + expect(members2.sort()).toEqual(["a", "b", "c", "d", "e", "f"]); + + // Store union with non-existent set + const count3 = await redis.sunionstore(dest, key1, "nonexistent"); + expect(count3).toBe(3); + + // Verify destination has only key1 members + const members3 = await redis.smembers(dest); + expect(members3.sort()).toEqual(["a", "b", "c"]); + + // Store empty union + const count4 = await redis.sunionstore(dest, "nonexistent1", "nonexistent2"); + expect(count4).toBe(0); + + // Verify destination is empty + const members4 = await redis.smembers(dest); + expect(members4).toEqual([]); + }); + }); + + describe("Sorted Set Operations", () => { + test("should get cardinality with ZCARD", async () => { + const redis = ctx.redis; + const key = "zcard-test"; + + // Empty set should return 0 + const count1 = await redis.zcard(key); + expect(count1).toBe(0); + + // Add members + await redis.zadd(key, 1, "one", 2, "two", 3, "three"); + + // Get cardinality + const count2 = await redis.zcard(key); + expect(count2).toBe(3); + + // Add duplicate (updates score, doesn't increase count) + await redis.zadd(key, 4, "one"); + const count3 = await redis.zcard(key); + expect(count3).toBe(3); + }); + + test("should pop max with ZPOPMAX", async () => { + const redis = ctx.redis; + const key = "zpopmax-test"; + + // Add members with scores + await redis.zadd(key, 1, "one", 2, "two", 3, "three", 4, "four"); + + // Pop one max member + const result1 = await redis.zpopmax(key); + expect(result1).toEqual(["four", 4]); + + // Pop two max members + const result2 = await redis.zpopmax(key, 2); + expect(result2).toEqual([ + ["three", 3], + ["two", 2], + ]); + + // Verify remaining member + const remaining = await redis.zcard(key); + expect(remaining).toBe(1); + + // Pop from empty set + await redis.zpopmax(key); + const empty = await redis.zpopmax(key); + expect(empty).toEqual([]); + }); + + test("should pop min with ZPOPMIN", async () => { + const redis = ctx.redis; + const key = "zpopmin-test"; + + // Add members with scores + await redis.zadd(key, 1, "one", 2, "two", 3, "three", 4, "four"); + + // Pop one min member + const result1 = await redis.zpopmin(key); + expect(result1).toEqual(["one", 1]); + + // Pop two min members + const result2 = await redis.zpopmin(key, 2); + expect(result2).toEqual([ + ["two", 2], + ["three", 3], + ]); + + // Verify remaining member + const remaining = await redis.zcard(key); + expect(remaining).toBe(1); + + // Pop from empty set + await redis.zpopmin(key); + const empty = await redis.zpopmin(key); + expect(empty).toEqual([]); + }); + + test("should get random member with ZRANDMEMBER", async () => { + const redis = ctx.redis; + const key = "zrandmember-test"; + + // Add members + await redis.zadd(key, 1, "one", 2, "two", 3, "three"); + + // Get one random member + const result1 = await redis.zrandmember(key); + expect(result1).toBeDefined(); + expect(["one", "two", "three"]).toContain(result1); + + // Get multiple random members (without scores) + const result2 = await redis.zrandmember(key, 2); + expect(Array.isArray(result2)).toBe(true); + expect(result2!.length).toBeLessThanOrEqual(2); + + result2!.forEach((member: string) => { + expect(["one", "two", "three"]).toContain(member); + }); + + // Get with scores + const result3 = await redis.zrandmember(key, 1, "WITHSCORES"); + + expect<([string, number][] | null)[]>([[["one", 1]], [["two", 2]], [["three", 3]]]).toContainEqual(result3); + + // Empty set + const emptyKey = "zrandmember-empty-" + randomUUIDv7(); + const empty = await redis.zrandmember(emptyKey); + expect(empty).toBeNull(); + }); + + test("should get rank with ZRANK", async () => { + const redis = ctx.redis; + const key = "zrank-test"; + + // Add members with scores + await redis.zadd(key, 1, "one", 2, "two", 3, "three"); + + // Get ranks (0-indexed, ascending) + const rank1 = await redis.zrank(key, "one"); + expect(rank1).toBe(0); + + const rank2 = await redis.zrank(key, "two"); + expect(rank2).toBe(1); + + const rank3 = await redis.zrank(key, "three"); + expect(rank3).toBe(2); + + // Non-existent member + const rank4 = await redis.zrank(key, "nonexistent"); + expect(rank4).toBeNull(); + }); + + test("should get reverse rank with ZREVRANK", async () => { + const redis = ctx.redis; + const key = "zrevrank-test"; + + // Add members with scores + await redis.zadd(key, 1, "one", 2, "two", 3, "three"); + + // Get reverse ranks (0-indexed, descending) + const rank1 = await redis.zrevrank(key, "three"); + expect(rank1).toBe(0); + + const rank2 = await redis.zrevrank(key, "two"); + expect(rank2).toBe(1); + + const rank3 = await redis.zrevrank(key, "one"); + expect(rank3).toBe(2); + + // Non-existent member + const rank4 = await redis.zrevrank(key, "nonexistent"); + expect(rank4).toBeNull(); + }); + + test("should increment score with ZINCRBY", async () => { + const redis = ctx.redis; + const key = "zincrby-test"; + + // Add initial members + await redis.send("ZADD", [key, "1.0", "member1", "2.0", "member2"]); + + // Increment member1's score by 2.5 + const newScore1 = await redis.zincrby(key, 2.5, "member1"); + expect(newScore1).toBe(3.5); + + // Increment member2's score by -1.5 + const newScore2 = await redis.zincrby(key, -1.5, "member2"); + expect(newScore2).toBe(0.5); + + // Increment non-existent member (should create it with the increment as score) + const newScore3 = await redis.zincrby(key, 5, "member3"); + expect(newScore3).toBe(5); + }); + + test("should count members in score range with ZCOUNT", async () => { + const redis = ctx.redis; + const key = "zcount-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Count all members + const count1 = await redis.zcount(key, "-inf", "+inf"); + expect(count1).toBe(5); + + // Count members with score between 2 and 4 (inclusive) + const count2 = await redis.zcount(key, 2, 4); + expect(count2).toBe(3); // two, three, four + + // Count with specific range + const count3 = await redis.zcount(key, 1, 3); + expect(count3).toBe(3); // one, two, three + + // Count with no matches + const count4 = await redis.zcount(key, 10, 20); + expect(count4).toBe(0); + }); + + test("should count members in lexicographical range with ZLEXCOUNT", async () => { + const redis = ctx.redis; + const key = "zlexcount-test"; + + // Add members with same score (required for lex operations) + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + + // Count all members + const count1 = await redis.zlexcount(key, "-", "+"); + expect(count1).toBe(5); + + // Count members from "banana" to "date" (inclusive) + const count2 = await redis.zlexcount(key, "[banana", "[date"); + expect(count2).toBe(3); // banana, cherry, date + + // Count with exclusive range + const count3 = await redis.zlexcount(key, "(banana", "(date"); + expect(count3).toBe(1); // only cherry + + // Count with no matches + const count4 = await redis.zlexcount(key, "[zebra", "[zoo"); + expect(count4).toBe(0); + }); + + test("should compute difference between sorted sets with ZDIFF", async () => { + const redis = ctx.redis; + const key1 = "zdiff-test1"; + const key2 = "zdiff-test2"; + const key3 = "zdiff-test3"; + + // Set up sorted sets + await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three", "4", "four"]); + await redis.send("ZADD", [key2, "1", "one", "2", "two"]); + await redis.send("ZADD", [key3, "3", "three"]); + + // Difference between first and second set + const diff1 = await redis.zdiff(2, key1, key2); + expect(diff1).toEqual(["three", "four"]); // Difference with multiple sets const diff2 = await redis.zdiff(3, key1, key2, key3); @@ -2378,52 +3878,42 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { await redis.zincrby({} as any, 1, "member"); - }).toThrowErrorMatchingInlineSnapshot( - `"Expected additional arguments to be a string or buffer for 'zincrby'."`, - ); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zincrby'."`); }); test("should reject invalid key in ZCOUNT", async () => { const redis = ctx.redis; expect(async () => { await redis.zcount([] as any, 0, 10); - }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zcount'."`); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zcount'."`); }); test("should reject invalid key in ZLEXCOUNT", async () => { const redis = ctx.redis; expect(async () => { await redis.zlexcount(null as any, "[a", "[z"); - }).toThrowErrorMatchingInlineSnapshot( - `"Expected additional arguments to be a string or buffer for 'zlexcount'."`, - ); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zlexcount'."`); }); test("should reject invalid key in ZREMRANGEBYRANK", async () => { const redis = ctx.redis; expect(async () => { await redis.zremrangebyrank({} as any, 0, 10); - }).toThrowErrorMatchingInlineSnapshot( - `"Expected additional arguments to be a string or buffer for 'zremrangebyrank'."`, - ); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zremrangebyrank'."`); }); test("should reject invalid key in ZREMRANGEBYSCORE", async () => { const redis = ctx.redis; expect(async () => { await redis.zremrangebyscore([] as any, 0, 10); - }).toThrowErrorMatchingInlineSnapshot( - `"Expected additional arguments to be a string or buffer for 'zremrangebyscore'."`, - ); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zremrangebyscore'."`); }); test("should reject invalid key in ZREMRANGEBYLEX", async () => { const redis = ctx.redis; expect(async () => { await redis.zremrangebylex(null as any, "[a", "[z"); - }).toThrowErrorMatchingInlineSnapshot( - `"Expected additional arguments to be a string or buffer for 'zremrangebylex'."`, - ); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zremrangebylex'."`); }); test("should remove one or more members with ZREM", async () => { @@ -2699,19 +4189,35 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { } await Promise.all(promises); - // Scan with COUNT hint - const [cursor, elements] = await redis.zscan(key, "0", "COUNT", "10"); + // Scan with COUNT hint - loop until cursor returns to "0" + let cursor = "0"; + const allElements: string[] = []; + do { + const [nextCursor, elements] = await redis.zscan(key, cursor, "COUNT", "10"); + allElements.push(...elements); + cursor = nextCursor; + } while (cursor !== "0"); + + // Should have member-score pairs (200 elements total: 100 members + 100 scores) + expect(allElements.length).toBe(200); + + // Verify we got all members (check every other element for member names) + const members = allElements.filter((_, index) => index % 2 === 0); + expect(members.length).toBe(100); + for (let i = 0; i < 100; i++) { + expect(members).toContain(`member:${i}`); + } - expect(cursor).toBe("0"); // Should complete in one scan since we know the size - // COUNT is a hint, so we might get more or fewer elements - // Just verify we got some elements back - expect(elements.length).toBeGreaterThan(0); - expect(Array.isArray(elements)).toBe(true); + // Test with numeric cursor + cursor = 0 as any; + const allElements2: string[] = []; + do { + const [nextCursor, elements] = await redis.zscan(key, cursor, "COUNT", "10"); + allElements2.push(...elements); + cursor = nextCursor; + } while (cursor !== "0"); - const [cursor2, elements2] = await redis.zscan(key, 0, "COUNT", "10"); - expect(cursor2).toBe("0"); - expect(elements2.length).toBeGreaterThan(0); - expect(Array.isArray(elements2)).toBe(true); + expect(allElements2.length).toBe(200); }); test("should reject invalid key in ZADD", async () => { @@ -2891,212 +4397,570 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should reject invalid key in ZREVRANGE", async () => { const redis = ctx.redis; - expect(async () => { - await redis.zrevrange([] as any, 0, 10); - }).toThrowErrorMatchingInlineSnapshot( - `"Expected additional arguments to be a string or buffer for 'zrevrange'."`, - ); + expect(async () => { + await redis.zrevrange([] as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrevrange'."`, + ); + }); + test("should return members by score range with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get all members + const all = await redis.zrangebyscore(key, "-inf", "+inf"); + expect(all).toEqual(["one", "two", "three", "four", "five"]); + + // Get members with score 2-4 (inclusive) + const range1 = await redis.zrangebyscore(key, 2, 4); + expect(range1).toEqual(["two", "three", "four"]); + + // Get members with exclusive lower bound + const range2 = await redis.zrangebyscore(key, "(2", 4); + expect(range2).toEqual(["three", "four"]); + + // Get members with exclusive upper bound + const range3 = await redis.zrangebyscore(key, 2, "(4"); + expect(range3).toEqual(["two", "three"]); + + // Get members with both exclusive bounds + const range4 = await redis.zrangebyscore(key, "(2", "(4"); + expect(range4).toEqual(["three"]); + }); + + test("should support WITHSCORES option with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-withscores-test"; + + // Add members + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + // Get with scores + const result = await redis.zrangebyscore(key, 1, 3, "WITHSCORES"); + expect(result).toEqual([ + ["one", 1.5], + ["two", 2.7], + ]); + }); + + test("should support LIMIT option with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-limit-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get first 2 members in score range + const limited1 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 0, 2); + expect(limited1).toEqual(["one", "two"]); + + // Skip first, get next 2 + const limited2 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 1, 2); + expect(limited2).toEqual(["two", "three"]); + + // Can combine with score range + const limited3 = await redis.zrangebyscore(key, 2, 5, "LIMIT", 1, 2); + expect(limited3).toEqual(["three", "four"]); + }); + + test("should support WITHSCORES with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-withscores-only-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); + + const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES"); + expect(result).toEqual([ + ["one", 1], + ["two", 2], + ["three", 3], + ["four", 4], + ]); + }); + + test("should support LIMIT and WITHSCORES together with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-combined-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); + + // Get with both options + const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES", "LIMIT", 1, 2); + expect(result).toEqual([ + ["two", 2], + ["three", 3], + ]); + }); + + test("should return members by score range in reverse with ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrevrangebyscore-test"; + + // Add members with scores (note: max comes before min in ZREVRANGEBYSCORE) + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get all members in reverse order + const all = await redis.zrevrangebyscore(key, "+inf", "-inf"); + expect(all).toEqual(["five", "four", "three", "two", "one"]); + + // Get members with score 4-2 (note: max=4, min=2) + const range1 = await redis.zrevrangebyscore(key, 4, 2); + expect(range1).toEqual(["four", "three", "two"]); + + // Get with exclusive bounds + const range2 = await redis.zrevrangebyscore(key, "(4", "(2"); + expect(range2).toEqual(["three"]); + + // Get with one exclusive bound + const range3 = await redis.zrevrangebyscore(key, 4, "(2"); + expect(range3).toEqual(["four", "three"]); + }); + + test("should support WITHSCORES option with ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrevrangebyscore-withscores-test"; + + // Add members + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + // Get with scores (max=3, min=1) + const result = await redis.zrevrangebyscore(key, 3, 1, "WITHSCORES"); + expect(result).toEqual([ + ["two", 2.7], + ["one", 1.5], + ]); + }); + + test("should support LIMIT option with ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrevrangebyscore-limit-test"; + + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Get first 2 members in reverse + const limited1 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 0, 2); + expect(limited1).toEqual(["five", "four"]); + + // Skip first, get next 2 + const limited2 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 1, 2); + expect(limited2).toEqual(["four", "three"]); + }); + + test("should return members by lexicographical range with ZRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zrangebylex-test"; + + // Add members with same score (required for lex operations) + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + + // Get all members + const all = await redis.zrangebylex(key, "-", "+"); + expect(all).toEqual(["apple", "banana", "cherry", "date", "elderberry"]); + + // Get range from "banana" to "date" (inclusive) + const range1 = await redis.zrangebylex(key, "[banana", "[date"); + expect(range1).toEqual(["banana", "cherry", "date"]); + + // Get range with exclusive bounds + const range2 = await redis.zrangebylex(key, "(banana", "(date"); + expect(range2).toEqual(["cherry"]); + + // Get range with one exclusive, one inclusive + const range3 = await redis.zrangebylex(key, "[banana", "(date"); + expect(range3).toEqual(["banana", "cherry"]); + + // Get range from start to specific member + const range4 = await redis.zrangebylex(key, "-", "[cherry"); + expect(range4).toEqual(["apple", "banana", "cherry"]); + + // Get range from specific member to end + const range5 = await redis.zrangebylex(key, "[cherry", "+"); + expect(range5).toEqual(["cherry", "date", "elderberry"]); + }); + + test("should support LIMIT option with ZRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zrangebylex-limit-test"; + + // Add members with same score + await redis.send("ZADD", [key, "0", "a", "0", "b", "0", "c", "0", "d", "0", "e", "0", "f", "0", "g"]); + + // Get first 3 members + const limited1 = await redis.zrangebylex(key, "-", "+", "LIMIT", 0, 3); + expect(limited1).toEqual(["a", "b", "c"]); + + // Skip first 2, get next 3 + const limited2 = await redis.zrangebylex(key, "-", "+", "LIMIT", 2, 3); + expect(limited2).toEqual(["c", "d", "e"]); + + // Get last 2 with large offset + const limited3 = await redis.zrangebylex(key, "-", "+", "LIMIT", 5, 10); + expect(limited3).toEqual(["f", "g"]); + }); + + test("should get cardinality of sorted set with ZCARD", async () => { + const redis = ctx.redis; + const key = "zcard-test"; + + // Check cardinality of non-existent key + const count0 = await redis.zcard(key); + expect(count0).toBe(0); + + // Add members to sorted set + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); + + // Check cardinality + const count1 = await redis.zcard(key); + expect(count1).toBe(3); + + // Add more members + await redis.send("ZADD", [key, "4", "four", "5", "five"]); + + // Check updated cardinality + const count2 = await redis.zcard(key); + expect(count2).toBe(5); + }); + + test("should pop member with lowest score using ZPOPMIN", async () => { + const redis = ctx.redis; + const key = "zpopmin-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result1 = await redis.zpopmin(key); + expect(result1).toEqual(["one", 1]); + + const result2 = await redis.zpopmin(key); + expect(result2).toEqual(["two", 2]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(3); + }); + + test("should pop multiple members with ZPOPMIN using COUNT", async () => { + const redis = ctx.redis; + const key = "zpopmin-count-test"; + + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + // Pop 3 members with lowest scores + const result = await redis.zpopmin(key, 3); + expect(result).toEqual([ + ["one", 1], + ["two", 2], + ["three", 3], + ]); + + // Verify remaining count + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should return empty array when ZPOPMIN on empty set", async () => { + const redis = ctx.redis; + const emptyKey = "zpopmin-empty-test"; + + // Try to pop from empty set + const result = await redis.zpopmin(emptyKey); + expect(result).toEqual([]); + }); + + test("should handle ties in score with ZPOPMIN", async () => { + const redis = ctx.redis; + const key = "zpopmin-tie-test"; + + // Add members with same scores + await redis.send("ZADD", [key, "1", "a", "1", "b", "1", "c", "2", "d"]); + + // Pop 2 members (should get lexicographically first members with same score) + const result = await redis.zpopmin(key, 2); + expect(result).toEqual([ + ["a", 1], + ["b", 1], + ]); + + // Verify remaining count + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should pop member with highest score using ZPOPMAX", async () => { + const redis = ctx.redis; + const key = "zpopmax-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result1 = await redis.zpopmax(key); + expect(result1).toEqual(["five", 5]); + + const result2 = await redis.zpopmax(key); + expect(result2).toEqual(["four", 4]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(3); }); - test("should return members by score range with ZRANGEBYSCORE", async () => { + + test("should pop multiple members with ZPOPMAX using COUNT", async () => { const redis = ctx.redis; - const key = "zrangebyscore-test"; + const key = "zpopmax-count-test"; // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get all members - const all = await redis.zrangebyscore(key, "-inf", "+inf"); - expect(all).toEqual(["one", "two", "three", "four", "five"]); - - // Get members with score 2-4 (inclusive) - const range1 = await redis.zrangebyscore(key, 2, 4); - expect(range1).toEqual(["two", "three", "four"]); + // Pop 3 members with highest scores + const result = await redis.zpopmax(key, 3); + expect(result).toEqual([ + ["five", 5], + ["four", 4], + ["three", 3], + ]); - // Get members with exclusive lower bound - const range2 = await redis.zrangebyscore(key, "(2", 4); - expect(range2).toEqual(["three", "four"]); + // Verify remaining count + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); - // Get members with exclusive upper bound - const range3 = await redis.zrangebyscore(key, 2, "(4"); - expect(range3).toEqual(["two", "three"]); + test("should return empty array when ZPOPMAX on empty set", async () => { + const redis = ctx.redis; + const emptyKey = "zpopmax-empty-test"; - // Get members with both exclusive bounds - const range4 = await redis.zrangebyscore(key, "(2", "(4"); - expect(range4).toEqual(["three"]); + // Try to pop from empty set + const result = await redis.zpopmax(emptyKey); + expect(result).toEqual([]); }); - test("should support WITHSCORES option with ZRANGEBYSCORE", async () => { + test("should handle ties in score with ZPOPMAX", async () => { const redis = ctx.redis; - const key = "zrangebyscore-withscores-test"; + const key = "zpopmax-tie-test"; - // Add members - await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + // Add members with same scores + await redis.send("ZADD", [key, "1", "a", "2", "b", "2", "c", "2", "d"]); - // Get with scores - const result = await redis.zrangebyscore(key, 1, 3, "WITHSCORES"); + // Pop 2 members (should get lexicographically last members with same score) + const result = await redis.zpopmax(key, 2); expect(result).toEqual([ - ["one", 1.5], - ["two", 2.7], + ["d", 2], + ["c", 2], ]); + + // Verify remaining count + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); }); - test("should support LIMIT option with ZRANGEBYSCORE", async () => { + test("should get random member with ZRANDMEMBER", async () => { const redis = ctx.redis; - const key = "zrangebyscore-limit-test"; + const key = "zrandmember-test"; // Add members await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get first 2 members in score range - const limited1 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 0, 2); - expect(limited1).toEqual(["one", "two"]); + // Get single random member + const result = await redis.zrandmember(key); + expect(result).toBeDefined(); + expect(typeof result).toBe("string"); + expect(["one", "two", "three", "four", "five"]).toContain(result); + }); - // Skip first, get next 2 - const limited2 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 1, 2); - expect(limited2).toEqual(["two", "three"]); + test("should return null when ZRANDMEMBER on empty set", async () => { + const redis = ctx.redis; + const emptyKey = "zrandmember-empty-test"; - // Can combine with score range - const limited3 = await redis.zrangebyscore(key, 2, 5, "LIMIT", 1, 2); - expect(limited3).toEqual(["three", "four"]); + // Try to get random member from empty set + const result = await redis.zrandmember(emptyKey); + expect(result).toBeNull(); }); - test("should support WITHSCORES with ZRANGEBYSCORE", async () => { + test("should get multiple random members with ZRANDMEMBER", async () => { const redis = ctx.redis; - const key = "zrangebyscore-withscores-only-test"; + const key = "zrandmember-count-test"; - await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); - const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES"); - expect(result).toEqual([ - ["one", 1], - ["two", 2], - ["three", 3], - ["four", 4], - ]); + const result = await redis.zrandmember(key, 2); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result!.length).toBe(2); + + for (const member of result!) { + expect(["one", "two", "three"]).toContain(member); + } + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(3); }); - test("should support LIMIT and WITHSCORES together with ZRANGEBYSCORE", async () => { + test("should get random members with scores using WITHSCORES in ZRANDMEMBER", async () => { const redis = ctx.redis; - const key = "zrangebyscore-combined-test"; + const key = "zrandmember-withscores-test"; // Add members - await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); - // Get with both options - const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES", "LIMIT", 1, 2); - expect(result).toEqual([ - ["two", 2], - ["three", 3], - ]); + // Get 2 random members with scores + const result = await redis.zrandmember(key, 2, "WITHSCORES"); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result!.length).toBe(2); + + for (const item of result!) { + expect(Array.isArray(item)).toBe(true); + expect(item.length).toBe(2); + expect(typeof item[0]).toBe("string"); + expect(typeof item[1]).toBe("number"); + expect(["one", "two", "three"]).toContain(item[0]); + } }); - test("should return members by score range in reverse with ZREVRANGEBYSCORE", async () => { + test("should allow negative count for ZRANDMEMBER to allow duplicates", async () => { const redis = ctx.redis; - const key = "zrevrangebyscore-test"; + const key = "zrandmember-negative-test"; - // Add members with scores (note: max comes before min in ZREVRANGEBYSCORE) + // Add members + await redis.send("ZADD", [key, "1", "one", "2", "two"]); + + // Get 5 random members with negative count (allows duplicates) + const result = await redis.zrandmember(key, -5); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result!.length).toBe(5); + + for (const member of result!) { + expect(["one", "two"]).toContain(member); + } + }); + + test("should get rank of member with ZRANK", async () => { + const redis = ctx.redis; + const key = "zrank-test"; + + // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get all members in reverse order - const all = await redis.zrevrangebyscore(key, "+inf", "-inf"); - expect(all).toEqual(["five", "four", "three", "two", "one"]); + // Get rank of members (0-indexed, lowest score has rank 0) + const rank1 = await redis.zrank(key, "one"); + expect(rank1).toBe(0); - // Get members with score 4-2 (note: max=4, min=2) - const range1 = await redis.zrevrangebyscore(key, 4, 2); - expect(range1).toEqual(["four", "three", "two"]); + const rank2 = await redis.zrank(key, "three"); + expect(rank2).toBe(2); - // Get with exclusive bounds - const range2 = await redis.zrevrangebyscore(key, "(4", "(2"); - expect(range2).toEqual(["three"]); + const rank3 = await redis.zrank(key, "five"); + expect(rank3).toBe(4); - // Get with one exclusive bound - const range3 = await redis.zrevrangebyscore(key, 4, "(2"); - expect(range3).toEqual(["four", "three"]); + // Non-existent member should return null + const rank4 = await redis.zrank(key, "nonexistent"); + expect(rank4).toBeNull(); }); - test("should support WITHSCORES option with ZREVRANGEBYSCORE", async () => { + test("should get rank with score using WITHSCORE in ZRANK", async () => { const redis = ctx.redis; - const key = "zrevrangebyscore-withscores-test"; + const key = "zrank-withscore-test"; - // Add members + // Add members with scores await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); - // Get with scores (max=3, min=1) - const result = await redis.zrevrangebyscore(key, 3, 1, "WITHSCORES"); - expect(result).toEqual([ - ["two", 2.7], - ["one", 1.5], - ]); + // Get rank with score + const result = await redis.zrank(key, "two", "WITHSCORE"); + expect(result).toEqual([1, 2.7]); + + // First member + const result2 = await redis.zrank(key, "one", "WITHSCORE"); + expect(result2).toEqual([0, 1.5]); + + // Non-existent member should return null + const result3 = await redis.zrank(key, "nonexistent", "WITHSCORE"); + expect(result3).toBeNull(); }); - test("should support LIMIT option with ZREVRANGEBYSCORE", async () => { + test("should handle ties in score with ZRANK", async () => { const redis = ctx.redis; - const key = "zrevrangebyscore-limit-test"; + const key = "zrank-tie-test"; - // Add members - await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + // Add members with same scores + await redis.send("ZADD", [key, "1", "a", "1", "b", "1", "c", "2", "d"]); - // Get first 2 members in reverse - const limited1 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 0, 2); - expect(limited1).toEqual(["five", "four"]); + // Members with same score are ordered lexicographically + const rankA = await redis.zrank(key, "a"); + expect(rankA).toBe(0); - // Skip first, get next 2 - const limited2 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 1, 2); - expect(limited2).toEqual(["four", "three"]); + const rankB = await redis.zrank(key, "b"); + expect(rankB).toBe(1); + + const rankC = await redis.zrank(key, "c"); + expect(rankC).toBe(2); + + const rankD = await redis.zrank(key, "d"); + expect(rankD).toBe(3); }); - test("should return members by lexicographical range with ZRANGEBYLEX", async () => { + test("should get reverse rank of member with ZREVRANK", async () => { const redis = ctx.redis; - const key = "zrangebylex-test"; + const key = "zrevrank-test"; - // Add members with same score (required for lex operations) - await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + // Add members with scores + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get all members - const all = await redis.zrangebylex(key, "-", "+"); - expect(all).toEqual(["apple", "banana", "cherry", "date", "elderberry"]); + // Get reverse rank (0-indexed from highest score) + const rank1 = await redis.zrevrank(key, "five"); + expect(rank1).toBe(0); - // Get range from "banana" to "date" (inclusive) - const range1 = await redis.zrangebylex(key, "[banana", "[date"); - expect(range1).toEqual(["banana", "cherry", "date"]); + const rank2 = await redis.zrevrank(key, "three"); + expect(rank2).toBe(2); - // Get range with exclusive bounds - const range2 = await redis.zrangebylex(key, "(banana", "(date"); - expect(range2).toEqual(["cherry"]); + const rank3 = await redis.zrevrank(key, "one"); + expect(rank3).toBe(4); - // Get range with one exclusive, one inclusive - const range3 = await redis.zrangebylex(key, "[banana", "(date"); - expect(range3).toEqual(["banana", "cherry"]); + // Non-existent member should return null + const rank4 = await redis.zrevrank(key, "nonexistent"); + expect(rank4).toBeNull(); + }); - // Get range from start to specific member - const range4 = await redis.zrangebylex(key, "-", "[cherry"); - expect(range4).toEqual(["apple", "banana", "cherry"]); + test("should get reverse rank with score using WITHSCORE in ZREVRANK", async () => { + const redis = ctx.redis; + const key = "zrevrank-withscore-test"; - // Get range from specific member to end - const range5 = await redis.zrangebylex(key, "[cherry", "+"); - expect(range5).toEqual(["cherry", "date", "elderberry"]); + // Add members with scores + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + // Get reverse rank with score + const result = await redis.zrevrank(key, "two", "WITHSCORE"); + expect(result).toEqual([1, 2.7]); + + // Highest score member (reverse rank 0) + const result2 = await redis.zrevrank(key, "three", "WITHSCORE"); + expect(result2).toEqual([0, 3.9]); + + // Non-existent member should return null + const result3 = await redis.zrevrank(key, "nonexistent", "WITHSCORE"); + expect(result3).toBeNull(); }); - test("should support LIMIT option with ZRANGEBYLEX", async () => { + test("should handle ties in score with ZREVRANK", async () => { const redis = ctx.redis; - const key = "zrangebylex-limit-test"; + const key = "zrevrank-tie-test"; - // Add members with same score - await redis.send("ZADD", [key, "0", "a", "0", "b", "0", "c", "0", "d", "0", "e", "0", "f", "0", "g"]); + // Add members with same scores + await redis.send("ZADD", [key, "1", "a", "2", "b", "2", "c", "2", "d"]); - // Get first 3 members - const limited1 = await redis.zrangebylex(key, "-", "+", "LIMIT", 0, 3); - expect(limited1).toEqual(["a", "b", "c"]); + // Members with same score are ordered lexicographically (reverse) + const rankD = await redis.zrevrank(key, "d"); + expect(rankD).toBe(0); - // Skip first 2, get next 3 - const limited2 = await redis.zrangebylex(key, "-", "+", "LIMIT", 2, 3); - expect(limited2).toEqual(["c", "d", "e"]); + const rankC = await redis.zrevrank(key, "c"); + expect(rankC).toBe(1); - // Get last 2 with large offset - const limited3 = await redis.zrangebylex(key, "-", "+", "LIMIT", 5, 10); - expect(limited3).toEqual(["f", "g"]); - }); + const rankB = await redis.zrevrank(key, "b"); + expect(rankB).toBe(2); + const rankA = await redis.zrevrank(key, "a"); + expect(rankA).toBe(3); + }); test("should reject invalid key in ZRANGEBYSCORE", async () => { const redis = ctx.redis; expect(async () => { @@ -3119,9 +4983,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { await redis.zrangebylex(null as any, "-", "+"); - }).toThrowErrorMatchingInlineSnapshot( - `"Expected additional arguments to be a string or buffer for 'zrangebylex'."`, - ); + }).toThrowErrorMatchingInlineSnapshot(`"The "key" argument must be specified"`); }); test("should return members in reverse lexicographical order with ZREVRANGEBYLEX", async () => { @@ -3718,17 +5580,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zmpop-min-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Pop member with lowest score const result1 = await redis.zmpop(1, key, "MIN"); - expect(result1).toBeDefined(); - expect(result1).not.toBeNull(); - expect(result1![0]).toBe(key); - expect(result1![1]).toEqual([["one", 1]]); + expect(result1).toEqual([key, [["one", 1]]]); - // Verify remaining count const count = await redis.send("ZCARD", [key]); expect(count).toBe(4); }); @@ -3737,17 +5593,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zmpop-max-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Pop member with highest score const result1 = await redis.zmpop(1, key, "MAX"); - expect(result1).toBeDefined(); - expect(result1).not.toBeNull(); - expect(result1![0]).toBe(key); - expect(result1![1]).toEqual([["five", 5]]); + expect(result1).toEqual([key, [["five", 5]]]); - // Verify remaining count const count = await redis.send("ZCARD", [key]); expect(count).toBe(4); }); @@ -3756,10 +5606,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zmpop-count-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Pop 3 members with lowest scores const result = await redis.zmpop(1, key, "MIN", "COUNT", 3); expect(result).toBeDefined(); expect(result).not.toBeNull(); @@ -3795,10 +5643,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { // Pop from multiple keys (should get from key2) const result = await redis.zmpop(3, key1, key2, key3, "MIN"); - expect(result).toBeDefined(); - expect(result).not.toBeNull(); - expect(result![0]).toBe(key2); - expect(result![1]).toEqual([["one", 1]]); + expect(result).toEqual([key2, [["one", 1]]]); }); test("should block and pop with BZMPOP", async () => { @@ -3810,12 +5655,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { // Use short timeout for testing const result = await redis.bzmpop(0.1, 1, key, "MIN"); - expect(result).toBeDefined(); - expect(result).not.toBeNull(); - expect(result![0]).toBe(key); - expect(result![1]).toEqual([["one", 1]]); + expect(result).toEqual([key, [["one", 1]]]); - // Verify member was removed const count = await redis.send("ZCARD", [key]); expect(count).toBe(1); }); @@ -3824,7 +5665,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const emptyKey = "bzmpop-timeout-test"; - // Try to pop from empty set with short timeout const result = await redis.bzmpop(0.1, 1, emptyKey, "MIN"); expect(result).toBeNull(); }); @@ -3833,36 +5673,92 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "bzmpop-count-test"; - // Add members to the set - await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); + + const result = await redis.bzmpop(0.5, 1, key, "MAX", "COUNT", 2); + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(result![0]).toBe(key); + expect(result![1]).toEqual([ + ["three", 3], + ["two", 2], + ]); + + // Verify one member remains + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(1); + }); + + test("should reject invalid arguments in ZMPOP", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zmpop({} as any, "key1", "MIN"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zmpop'."`); + }); + + test("should reject invalid arguments in BZMPOP", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.bzmpop(1, {} as any, "key1", "MIN"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'bzmpop'."`); + }); + + test("should pop member with highest score using ZPOPMAX", async () => { + const redis = ctx.redis; + const key = "zpopmax-test"; + + // Add members to sorted set + await redis.zadd(key, 1.0, "one", 2.0, "two", 3.0, "three"); + + // Pop highest score + const result = await redis.zpopmax(key); + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(2); + expect(result![0]).toBe("three"); + expect(result![1]).toBe(3); + + // Verify member was removed + const count = await redis.zcard(key); + expect(count).toBe(2); + }); + + test("should pop member with lowest score using ZPOPMIN", async () => { + const redis = ctx.redis; + const key = "zpopmin-test"; + + // Add members to sorted set + await redis.zadd(key, 1.0, "one", 2.0, "two", 3.0, "three"); - // Pop 2 members with short timeout - const result = await redis.bzmpop(0.5, 1, key, "MAX", "COUNT", 2); + // Pop lowest score + const result = await redis.zpopmin(key); expect(result).toBeDefined(); expect(result).not.toBeNull(); - expect(result![0]).toBe(key); - expect(result![1]).toEqual([ - ["three", 3], - ["two", 2], - ]); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(2); + expect(result![0]).toBe("one"); + expect(result![1]).toBe(1); - // Verify one member remains - const count = await redis.send("ZCARD", [key]); - expect(count).toBe(1); + // Verify member was removed + const count = await redis.zcard(key); + expect(count).toBe(2); }); - test("should reject invalid arguments in ZMPOP", async () => { + test("should return empty array for ZPOPMAX on empty set", async () => { const redis = ctx.redis; - expect(async () => { - await redis.zmpop({} as any, "key1", "MIN"); - }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zmpop'."`); + const key = "zpopmax-empty-test"; + + const result = await redis.zpopmax(key); + expect(result).toEqual([]); }); - test("should reject invalid arguments in BZMPOP", async () => { + test("should return empty array for ZPOPMIN on empty set", async () => { const redis = ctx.redis; - expect(async () => { - await redis.bzmpop(1, {} as any, "key1", "MIN"); - }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'bzmpop'."`); + const key = "zpopmin-empty-test"; + + const result = await redis.zpopmin(key); + expect(result).toEqual([]); }); test("should block and pop lowest score with BZPOPMIN", async () => { @@ -3975,6 +5871,210 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); describe("Hash Operations", () => { + test("should increment hash field by integer with HINCRBY", async () => { + const redis = ctx.redis; + const key = "hincrby-test"; + + // Increment non-existent field (creates it) + const val1 = await redis.hincrby(key, "field1", 5); + expect(val1).toBe(5); + + // Increment existing field + const val2 = await redis.hincrby(key, "field1", 3); + expect(val2).toBe(8); + + // Decrement + const val3 = await redis.hincrby(key, "field1", -2); + expect(val3).toBe(6); + }); + + test("should increment hash field by float with HINCRBYFLOAT", async () => { + const redis = ctx.redis; + const key = "hincrbyfloat-test"; + + const val1 = await redis.hincrbyfloat(key, "field1", 2.5); + expect(val1).toBe("2.5"); + + const val2 = await redis.hincrbyfloat(key, "field1", 1.3); + expect(Number.parseFloat(val2)).toBeCloseTo(3.8); + + const val3 = await redis.hincrbyfloat(key, "field1", -0.8); + expect(Number.parseFloat(val3)).toBeCloseTo(3.0); + }); + + test("should get all hash keys with HKEYS", async () => { + const redis = ctx.redis; + const key = "hkeys-test"; + + // Empty hash + const keys1 = await redis.hkeys(key); + expect(keys1).toEqual([]); + + // Add fields + await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); + + const keys2 = await redis.hkeys(key); + expect(keys2.sort()).toEqual(["field1", "field2", "field3"]); + }); + + test("should get hash length with HLEN", async () => { + const redis = ctx.redis; + const key = "hlen-test"; + + // Empty hash + const len1 = await redis.hlen(key); + expect(len1).toBe(0); + + // Add fields + await redis.hset(key, "field1", "value1", "field2", "value2"); + const len2 = await redis.hlen(key); + expect(len2).toBe(2); + + // Add more + await redis.hset(key, "field3", "value3"); + const len3 = await redis.hlen(key); + expect(len3).toBe(3); + }); + + test("should get multiple hash values with HMGET", async () => { + const redis = ctx.redis; + const key = "hmget-test"; + + await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); + + // Get multiple fields + const values = await redis.hmget(key, "field1", "field2", "field3"); + expect(values).toEqual(["value1", "value2", "value3"]); + + // Mix existing and non-existing + const mixed = await redis.hmget(key, "field1", "nonexistent", "field2"); + expect(mixed).toEqual(["value1", null, "value2"]); + }); + + test("should get all hash values with HVALS", async () => { + const redis = ctx.redis; + const key = "hvals-test"; + + // Empty hash + const vals1 = await redis.hvals(key); + expect(vals1).toEqual([]); + + // Add fields + await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); + + const vals2 = await redis.hvals(key); + expect(vals2.sort()).toEqual(["value1", "value2", "value3"]); + }); + + test("should get hash field string length with HSTRLEN", async () => { + const redis = ctx.redis; + const key = "hstrlen-test"; + + await redis.hset(key, "field1", "Hello", "field2", "World!"); + + const len1 = await redis.hstrlen(key, "field1"); + expect(len1).toBe(5); + + const len2 = await redis.hstrlen(key, "field2"); + expect(len2).toBe(6); + + // Non-existent field + const len3 = await redis.hstrlen(key, "nonexistent"); + expect(len3).toBe(0); + }); + + test("should set hash field expiration with HEXPIRE", async () => { + const redis = ctx.redis; + const key = "hexpire-test"; + + await redis.hset(key, "field1", "value1", "field2", "value2"); + + // Set expiration on field1 + const result = await redis.hexpire(key, 60, "FIELDS", 1, "field1"); + expect(result).toEqual([1]); + + // Verify TTL is set + const ttl = await redis.httl(key, "FIELDS", 1, "field1"); + expect(ttl[0]).toBeGreaterThan(0); + expect(ttl[0]).toBeLessThanOrEqual(60); + }); + + test("should set hash field expiration at timestamp with HEXPIREAT", async () => { + const redis = ctx.redis; + const key = "hexpireat-test"; + + await redis.hset(key, "field1", "value1"); + + const futureTs = Math.floor(Date.now() / 1000) + 60; + const result = await redis.hexpireat(key, futureTs, "FIELDS", 1, "field1"); + expect(result).toEqual([1]); + + const ttl = await redis.httl(key, "FIELDS", 1, "field1"); + expect(ttl[0]).toBeGreaterThan(0); + }); + + test("should get hash field expiration time with HEXPIRETIME", async () => { + const redis = ctx.redis; + const key = "hexpiretime-test"; + + await redis.hset(key, "field1", "value1"); + + const futureTs = Math.floor(Date.now() / 1000) + 60; + await redis.hexpireat(key, futureTs, "FIELDS", 1, "field1"); + + const expireTime = await redis.hexpiretime(key, "FIELDS", 1, "field1"); + expect(expireTime[0]).toBeGreaterThan(0); + expect(expireTime[0]).toBeLessThanOrEqual(futureTs); + }); + + test("should remove hash field expiration with HPERSIST", async () => { + const redis = ctx.redis; + const key = "hpersist-test"; + + await redis.hset(key, "field1", "value1"); + await redis.hexpire(key, 60, "FIELDS", 1, "field1"); + + // Verify expiration is set + const ttlBefore = await redis.httl(key, "FIELDS", 1, "field1"); + expect(ttlBefore[0]).toBeGreaterThan(0); + + // Remove expiration + const result = await redis.hpersist(key, "FIELDS", 1, "field1"); + expect(result).toEqual([1]); + + // Verify no expiration + const ttlAfter = await redis.httl(key, "FIELDS", 1, "field1"); + expect(ttlAfter[0]).toBe(-1); + }); + + test("should set hash field expiration at timestamp in ms with HPEXPIREAT", async () => { + const redis = ctx.redis; + const key = "hpexpireat-test"; + + await redis.hset(key, "field1", "value1"); + + const futureTs = Date.now() + 5000; + const result = await redis.hpexpireat(key, futureTs, "FIELDS", 1, "field1"); + expect(result).toEqual([1]); + + const pttl = await redis.hpttl(key, "FIELDS", 1, "field1"); + expect(pttl[0]).toBeGreaterThan(0); + }); + + test("should get hash field expiration time in ms with HPEXPIRETIME", async () => { + const redis = ctx.redis; + const key = "hpexpiretime-test"; + + await redis.hset(key, "field1", "value1"); + + const futureTs = Date.now() + 5000; + await redis.hpexpireat(key, futureTs, "FIELDS", 1, "field1"); + + const pexpireTime = await redis.hpexpiretime(key, "FIELDS", 1, "field1"); + expect(pexpireTime[0]).toBeGreaterThan(0); + expect(pexpireTime[0]).toBeLessThanOrEqual(futureTs); + }); + test("should set hash fields using object syntax", async () => { const redis = ctx.redis; const key = "hash-object-test"; @@ -4593,6 +6693,286 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { // COUNT is a hint, so we just check we got some results expect(result.length).toBeGreaterThan(0); }); + + test("should increment hash field by integer using hincrby", async () => { + const redis = ctx.redis; + const key = "hincrby-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { counter: "10" }); + + const result1 = await redis.hincrby(key, "counter", 5); + expect(result1).toBe(15); + + const result2 = await redis.hincrby(key, "counter", -3); + expect(result2).toBe(12); + + const value = await redis.hget(key, "counter"); + expect(value).toBe("12"); + }); + + test("should increment hash field from zero using hincrby", async () => { + const redis = ctx.redis; + const key = "hincrby-zero-test:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hincrby(key, "newfield", 42); + expect(result).toBe(42); + + const value = await redis.hget(key, "newfield"); + expect(value).toBe("42"); + }); + + test("should increment hash field by float using hincrbyfloat", async () => { + const redis = ctx.redis; + const key = "hincrbyfloat-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { price: "10.5" }); + + const result1 = await redis.hincrbyfloat(key, "price", 2.3); + expect(result1).toBe("12.8"); + + const result2 = await redis.hincrbyfloat(key, "price", -0.8); + expect(result2).toBe("12"); + + const value = await redis.hget(key, "price"); + expect(value).toBe("12"); + }); + + test("should increment hash field from zero using hincrbyfloat", async () => { + const redis = ctx.redis; + const key = "hincrbyfloat-zero-test:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hincrbyfloat(key, "newfield", 3.14); + expect(result).toBe("3.14"); + + const value = await redis.hget(key, "newfield"); + expect(value).toBe("3.14"); + }); + + test("should get all hash keys using hkeys", async () => { + const redis = ctx.redis; + const key = "hkeys-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const keys = await redis.hkeys(key); + expect(keys).toBeInstanceOf(Array); + expect(keys.length).toBe(3); + expect(keys).toContain("name"); + expect(keys).toContain("age"); + expect(keys).toContain("city"); + }); + + test("should return empty array for non-existent key using hkeys", async () => { + const redis = ctx.redis; + const key = "hkeys-nonexistent:" + randomUUIDv7().substring(0, 8); + + const keys = await redis.hkeys(key); + expect(keys).toEqual([]); + }); + + test("should get hash length using hlen", async () => { + const redis = ctx.redis; + const key = "hlen-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const length = await redis.hlen(key); + expect(length).toBe(3); + + await redis.hset(key, { country: "USA" }); + const newLength = await redis.hlen(key); + expect(newLength).toBe(4); + }); + + test("should return 0 for non-existent key using hlen", async () => { + const redis = ctx.redis; + const key = "hlen-nonexistent:" + randomUUIDv7().substring(0, 8); + + const length = await redis.hlen(key); + expect(length).toBe(0); + }); + + test("should get multiple hash values using hmget", async () => { + const redis = ctx.redis; + const key = "hmget-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const values = await redis.hmget(key, ["name", "age", "city"]); + expect(values).toEqual(["John", "30", "NYC"]); + }); + + test("should return null for missing fields using hmget", async () => { + const redis = ctx.redis; + const key = "hmget-missing-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const values = await redis.hmget(key, ["name", "age", "city"]); + expect(values).toEqual(["John", null, null]); + }); + + test("should get all hash values using hvals", async () => { + const redis = ctx.redis; + const key = "hvals-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const values = await redis.hvals(key); + expect(values).toBeInstanceOf(Array); + expect(values.length).toBe(3); + expect(values).toContain("John"); + expect(values).toContain("30"); + expect(values).toContain("NYC"); + }); + + test("should return empty array for non-existent key using hvals", async () => { + const redis = ctx.redis; + const key = "hvals-nonexistent:" + randomUUIDv7().substring(0, 8); + + const values = await redis.hvals(key); + expect(values).toEqual([]); + }); + + test("should get hash field string length using hstrlen", async () => { + const redis = ctx.redis; + const key = "hstrlen-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", description: "Software Engineer" }); + + const nameLen = await redis.hstrlen(key, "name"); + expect(nameLen).toBe(4); // "John" has 4 characters + + const descLen = await redis.hstrlen(key, "description"); + expect(descLen).toBe(17); // "Software Engineer" has 17 characters + }); + + test("should return 0 for non-existent field using hstrlen", async () => { + const redis = ctx.redis; + const key = "hstrlen-nonexistent:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const length = await redis.hstrlen(key, "age"); + expect(length).toBe(0); + }); + + test("should expire hash fields using hexpire", async () => { + const redis = ctx.redis; + const key = "hexpire-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const result = await redis.hexpire(key, 10, "FIELDS", 2, "name", "age"); + expect(result).toEqual([1, 1]); + + const ttls = await redis.httl(key, "FIELDS", 3, "name", "age", "city"); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(10); + expect(ttls[1]).toBeGreaterThan(0); + expect(ttls[1]).toBeLessThanOrEqual(10); + expect(ttls[2]).toBe(-1); // city has no expiration + }); + + test("should expire hash fields with NX flag using hexpire", async () => { + const redis = ctx.redis; + const key = "hexpire-nx-test:" + randomUUIDv7().substring(0, 8); + + await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); + await redis.hset(key, { age: "30" }); + + const result = await redis.hexpire(key, 10, "NX", "FIELDS", 2, "name", "age"); + expect(result).toEqual([0, 1]); // name already has TTL (NX fails), age doesn't (NX succeeds) + }); + + test("should expire hash fields at specific time using hexpireat", async () => { + const redis = ctx.redis; + const key = "hexpireat-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; // 60 seconds from now + const result = await redis.hexpireat(key, futureTimestamp, "FIELDS", 2, "name", "age"); + expect(result).toEqual([1, 1]); + + const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(60); + expect(ttls[1]).toBeGreaterThan(0); + expect(ttls[1]).toBeLessThanOrEqual(60); + }); + + test("should get hash field expiration time using hexpiretime", async () => { + const redis = ctx.redis; + const key = "hexpiretime-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const futureTimestamp = Math.floor(Date.now() / 1000) + 100; + await redis.hexpireat(key, futureTimestamp, "FIELDS", 1, "name"); + + const expiretimes = await redis.hexpiretime(key, "FIELDS", 2, "name", "age"); + expect(expiretimes).toHaveLength(2); + expect(expiretimes[0]).toBeGreaterThan(0); + expect(expiretimes[0]).toBeLessThanOrEqual(futureTimestamp); + expect(expiretimes[1]).toBe(-1); // age has no expiration + }); + + test("should expire hash fields at specific time in milliseconds using hpexpireat", async () => { + const redis = ctx.redis; + const key = "hpexpireat-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const futureTimestamp = Date.now() + 60000; // 60 seconds from now + const result = await redis.hpexpireat(key, futureTimestamp, "FIELDS", 2, "name", "age"); + expect(result).toEqual([1, 1]); + + const ttls = await redis.hpttl(key, "FIELDS", 2, "name", "age"); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(60000); + expect(ttls[1]).toBeGreaterThan(0); + expect(ttls[1]).toBeLessThanOrEqual(60000); + }); + + test("should get hash field expiration time in milliseconds using hpexpiretime", async () => { + const redis = ctx.redis; + const key = "hpexpiretime-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const futureTimestamp = Date.now() + 100000; + await redis.hpexpireat(key, futureTimestamp, "FIELDS", 1, "name"); + + const expiretimes = await redis.hpexpiretime(key, "FIELDS", 2, "name", "age"); + expect(expiretimes).toHaveLength(2); + expect(expiretimes[0]).toBeGreaterThan(0); + expect(expiretimes[0]).toBeLessThanOrEqual(futureTimestamp); + expect(expiretimes[1]).toBe(-1); // age has no expiration + }); + + test("should persist hash fields using hpersist", async () => { + const redis = ctx.redis; + const key = "hpersist-test:" + randomUUIDv7().substring(0, 8); + + await redis.hsetex(key, "EX", 100, "FIELDS", 2, "name", "John", "age", "30"); + + const result = await redis.hpersist(key, "FIELDS", 2, "name", "age"); + expect(result).toEqual([1, 1]); // Both fields had TTL and were persisted + + const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); + expect(ttls).toEqual([-1, -1]); // Both fields now have no expiration + }); + + test("should return 0 for fields without expiration using hpersist", async () => { + const redis = ctx.redis; + const key = "hpersist-noexpire-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const result = await redis.hpersist(key, "FIELDS", 2, "name", "age"); + expect(result).toEqual([-1, -1]); // Neither field had TTL (returns -1 for no TTL) + }); }); describe("Connection State", () => { From 1e16e7e535d0dfe7997a61a4afc6eb625fde7c11 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 03:57:14 -0700 Subject: [PATCH 37/45] fix: hmget is variadic --- test/js/valkey/valkey.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 0a5d93418373..d2f5dfad6f0b 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -6798,7 +6798,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, { name: "John", age: "30", city: "NYC" }); - const values = await redis.hmget(key, ["name", "age", "city"]); + const values = await redis.hmget(key, "name", "age", "city"); expect(values).toEqual(["John", "30", "NYC"]); }); @@ -6808,7 +6808,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, { name: "John" }); - const values = await redis.hmget(key, ["name", "age", "city"]); + const values = await redis.hmget(key, "name", "age", "city"); expect(values).toEqual(["John", null, null]); }); From ec3d7c270c7424b742a9fc69f9bf4c2886d05ea3 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 03:58:46 -0700 Subject: [PATCH 38/45] deflake --- test/js/valkey/valkey.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index d2f5dfad6f0b..c6abd05b0ea8 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -6930,9 +6930,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const ttls = await redis.hpttl(key, "FIELDS", 2, "name", "age"); expect(ttls[0]).toBeGreaterThan(0); - expect(ttls[0]).toBeLessThanOrEqual(60000); + expect(ttls[0]).toBeLessThanOrEqual(60100); // Small buffer for timing expect(ttls[1]).toBeGreaterThan(0); - expect(ttls[1]).toBeLessThanOrEqual(60000); + expect(ttls[1]).toBeLessThanOrEqual(60100); // Small buffer for timing }); test("should get hash field expiration time in milliseconds using hpexpiretime", async () => { From fd0b8070ed5f1847ac3a5168be1194fe7508f38c Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 04:11:23 -0700 Subject: [PATCH 39/45] update types --- packages/bun-types/redis.d.ts | 9 ++++++++- test/js/valkey/valkey.test.ts | 4 ---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 2960792bfebb..0d71188a110b 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -1262,6 +1262,13 @@ declare module "bun" { */ rpop(key: RedisClient.KeyLike): Promise; + /** + * Remove and get the last element in a list + * @param key The list key + * @returns Promise that resolves with the last element, or null if the list is empty + */ + rpop(key: RedisClient.KeyLike, count: number): Promise; + /** * Atomically pop the last element from a source list and push it to the head of a destination list * @@ -1875,7 +1882,7 @@ declare module "bun" { * @returns Promise that resolves with the length of the list after the push * operation */ - rpush(key: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + rpush(key: RedisClient.KeyLike, value: RedisClient.KeyLike, ...rest: RedisClient.KeyLike[]): Promise; /** * Append a value to a list, only if the list exists diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index c6abd05b0ea8..abef9fdd54c8 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -1491,18 +1491,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "rpop-test"; - // Add elements await redis.rpush(key, "one", "two", "three"); - // Pop one element const elem1 = await redis.rpop(key); expect(elem1).toBe("three"); - // Pop with count const elem2 = await redis.rpop(key, 2); expect(elem2).toEqual(["two", "one"]); - // Pop from empty list const empty = await redis.rpop(key); expect(empty).toBeNull(); }); From 9c57e0b3bc440eb73508ac3b24e3791d4a7b5f5a Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 04:15:07 -0700 Subject: [PATCH 40/45] rm --- test/js/valkey/valkey.test.ts | 1071 +++------------------------------ 1 file changed, 83 insertions(+), 988 deletions(-) diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index abef9fdd54c8..6c40be2f2c32 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -20,7 +20,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const ctx = { ..._ctx, redis: connectionType ? _ctx.redis : (_ctx.redisTLS as RedisClient) }; describe.skipIf(!isEnabled)(`Valkey Redis Client (${connectionType})`, () => { beforeAll(async () => { - // Ensure container is ready before tests run await setupDockerContainer(); if (!ctx.redis) { ctx.redis = createClient(connectionType); @@ -28,12 +27,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); beforeEach(async () => { - // Don't create a new client, just ensure we have one if (!ctx.redis) { ctx.redis = createClient(connectionType); } - // Flush all data for clean test state await ctx.redis.connect(); await ctx.redis.send("FLUSHALL", ["SYNC"]); }); @@ -44,51 +41,44 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const testKey = "greeting"; const testValue = "Hello from Bun Redis!"; - // Using direct set and get methods const setResult = await redis.set(testKey, testValue); expect(setResult).toMatchInlineSnapshot(`"OK"`); const setResult2 = await redis.set(testKey, testValue, "GET"); expect(setResult2).toMatchInlineSnapshot(`"${testValue}"`); - // GET should return the value we set const getValue = await redis.get(testKey); expect(getValue).toMatchInlineSnapshot(`"${testValue}"`); }); test("should test key existence", async () => { const redis = ctx.redis; - // Let's set a key first + await redis.set("greeting", "test existence"); - // EXISTS in Redis normally returns integer 1 if key exists, 0 if not - // The current implementation doesn't transform exists correctly yet const exists = await redis.exists("greeting"); expect(exists).toBeDefined(); - // Should be true for existing keys (fixed in special handling for EXISTS) + expect(exists).toBe(true); - // For non-existent keys const randomKey = "nonexistent-key-" + randomUUIDv7(); const notExists = await redis.exists(randomKey); expect(notExists).toBeDefined(); - // Should be false for non-existing keys + expect(notExists).toBe(false); }); test("should increment and decrement counters", async () => { const redis = ctx.redis; const counterKey = "counter"; - // First set a counter value + await redis.set(counterKey, "10"); - // INCR should increment and return the new value const incrementedValue = await redis.incr(counterKey); expect(incrementedValue).toBeDefined(); expect(typeof incrementedValue).toBe("number"); expect(incrementedValue).toBe(11); - // DECR should decrement and return the new value const decrementedValue = await redis.decr(counterKey); expect(decrementedValue).toBeDefined(); expect(typeof decrementedValue).toBe("number"); @@ -100,15 +90,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const counterKey = "incrby-counter"; await redis.set(counterKey, "5"); - // INCRBY should increment by the specified amount const result1 = await redis.incrby(counterKey, 10); expect(result1).toBe(15); - // INCRBY with negative value should decrement const result2 = await redis.incrby(counterKey, -3); expect(result2).toBe(12); - // INCRBY on non-existent key should treat it as 0 const result3 = await redis.incrby("new-incrby-key", 5); expect(result3).toBe(5); }); @@ -118,15 +105,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const floatKey = "float-counter"; await redis.set(floatKey, "10.5"); - // INCRBYFLOAT should increment by the specified float amount const result1 = await redis.incrbyfloat(floatKey, 2.3); expect(result1).toBe("12.8"); - // INCRBYFLOAT with negative value should decrement const result2 = await redis.incrbyfloat(floatKey, -0.8); expect(result2).toBe("12"); - // INCRBYFLOAT on non-existent key should treat it as 0 const result3 = await redis.incrbyfloat("new-float-key", 3.14); expect(result3).toBe("3.14"); }); @@ -136,15 +120,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const counterKey = "decrby-counter"; await redis.set(counterKey, "20"); - // DECRBY should decrement by the specified amount const result1 = await redis.decrby(counterKey, 5); expect(result1).toBe(15); - // DECRBY with larger value const result2 = await redis.decrby(counterKey, 10); expect(result2).toBe(5); - // DECRBY on non-existent key should treat it as 0 const result3 = await redis.decrby("new-decrby-key", 3); expect(result3).toBe(-3); }); @@ -155,18 +136,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const newKey = "new-key"; const value = "test-value"; - // Set a value on the old key await redis.set(oldKey, value); - // Rename the key const result = await redis.rename(oldKey, newKey); expect(result).toBe("OK"); - // The new key should have the value const newValue = await redis.get(newKey); expect(newValue).toBe(value); - // The old key should no longer exist const oldValue = await redis.get(oldKey); expect(oldValue).toBeNull(); }); @@ -176,19 +153,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const oldKey = "old-key-overwrite"; const newKey = "new-key-overwrite"; - // Set values on both keys await redis.set(oldKey, "old-value"); await redis.set(newKey, "existing-value"); - // Rename should overwrite the existing key const result = await redis.rename(oldKey, newKey); expect(result).toBe("OK"); - // The new key should have the old value const newValue = await redis.get(newKey); expect(newValue).toBe("old-value"); - // The old key should no longer exist const oldValue = await redis.get(oldKey); expect(oldValue).toBeNull(); }); @@ -199,18 +172,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const newKey = "new-key-nx"; const value = "test-value"; - // Set a value on the old key await redis.set(oldKey, value); - // RENAMENX should succeed (newkey doesn't exist) const result1 = await redis.renamenx(oldKey, newKey); expect(result1).toBe(1); - // The new key should have the value const newValue = await redis.get(newKey); expect(newValue).toBe(value); - // The old key should no longer exist const oldValue = await redis.get(oldKey); expect(oldValue).toBeNull(); }); @@ -220,15 +189,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const oldKey = "old-key-nx-fail"; const newKey = "new-key-nx-fail"; - // Set values on both keys await redis.set(oldKey, "old-value"); await redis.set(newKey, "existing-value"); - // RENAMENX should fail (newkey exists) const result = await redis.renamenx(oldKey, newKey); expect(result).toBe(0); - // Both keys should retain their original values const oldValue = await redis.get(oldKey); expect(oldValue).toBe("old-value"); @@ -239,11 +205,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should set multiple keys with MSET", async () => { const redis = ctx.redis; - // MSET should set multiple keys atomically const result = await redis.mset("mset-key1", "value1", "mset-key2", "value2", "mset-key3", "value3"); expect(result).toBe("OK"); - // Verify all keys were set const value1 = await redis.get("mset-key1"); expect(value1).toBe("value1"); const value2 = await redis.get("mset-key2"); @@ -255,45 +219,38 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should set multiple keys only if none exist with MSETNX", async () => { const redis = ctx.redis; - // First MSETNX should succeed (keys don't exist) const result1 = await redis.msetnx("msetnx-key1", "value1", "msetnx-key2", "value2"); expect(result1).toBe(1); - // Verify keys were set const value1 = await redis.get("msetnx-key1"); expect(value1).toBe("value1"); const value2 = await redis.get("msetnx-key2"); expect(value2).toBe("value2"); - // Second MSETNX should fail (at least one key exists) const result2 = await redis.msetnx("msetnx-key1", "newvalue", "msetnx-key3", "value3"); expect(result2).toBe(0); - // Verify original values weren't changed const unchangedValue = await redis.get("msetnx-key1"); expect(unchangedValue).toBe("value1"); - // And new key wasn't created const nonExistentKey = await redis.get("msetnx-key3"); expect(nonExistentKey).toBeNull(); }); test("should manage key expiration", async () => { const redis = ctx.redis; - // Set a key first + const tempKey = "temporary"; await redis.set(tempKey, "will expire"); - // EXPIRE should return 1 if the timeout was set, 0 otherwise const result = await redis.expire(tempKey, 60); - // Using native expire command instead of send() + expect(result).toMatchInlineSnapshot(`1`); - // Use the TTL command directly const ttl = await redis.ttl(tempKey); expectType(ttl, "number"); expect(ttl).toBeGreaterThan(0); - expect(ttl).toBeLessThanOrEqual(60); // Should be positive and not exceed our set time + expect(ttl).toBeLessThanOrEqual(60); }); test("should set key with expiration using SETEX", async () => { @@ -301,15 +258,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "setex-test-key"; const value = "test-value"; - // SETEX should set the key with expiration in seconds const result = await redis.setex(key, 10, value); expect(result).toBe("OK"); - // Verify the value was set const getValue = await redis.get(key); expect(getValue).toBe(value); - // Verify TTL is set (should be <= 10 seconds) const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(10); @@ -320,15 +274,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "psetex-test-key"; const value = "test-value"; - // PSETEX should set the key with expiration in milliseconds const result = await redis.psetex(key, 5000, value); expect(result).toBe("OK"); - // Verify the value was set const getValue = await redis.get(key); expect(getValue).toBe(value); - // Verify TTL is set (should be <= 5000 milliseconds, i.e., <= 5 seconds) const pttl = await redis.pttl(key); expect(pttl).toBeGreaterThan(0); expect(pttl).toBeLessThanOrEqual(5000); @@ -339,12 +290,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "expireat-test-key"; await redis.set(key, "test-value"); - // Set expiration to 60 seconds from now using Unix timestamp const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const result = await redis.expireat(key, futureTimestamp); - expect(result).toBe(1); // 1 indicates success + expect(result).toBe(1); - // Verify TTL is set (should be around 60 seconds) const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(60); @@ -354,7 +303,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const result = await redis.expireat("nonexistent-expireat-key", futureTimestamp); - expect(result).toBe(0); // 0 indicates key does not exist + expect(result).toBe(0); }); test("should set expiration with PEXPIRE in milliseconds", async () => { @@ -362,9 +311,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "pexpire-test-key"; await redis.set(key, "test-value"); - // Set expiration to 5000 milliseconds (5 seconds) const result = await redis.pexpire(key, 5000); - expect(result).toBe(1); // 1 indicates success + expect(result).toBe(1); const pttl = await redis.pttl(key); expect(pttl).toBeGreaterThan(0); @@ -374,7 +322,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should return 0 for PEXPIRE on non-existent key", async () => { const redis = ctx.redis; const result = await redis.pexpire("nonexistent-pexpire-key", 5000); - expect(result).toBe(0); // 0 indicates key does not exist + expect(result).toBe(0); }); test("should set expiration with PEXPIREAT using Unix timestamp in milliseconds", async () => { @@ -382,10 +330,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "pexpireat-test-key"; await redis.set(key, "test-value"); - // Set expiration to 5000 ms from now using Unix timestamp in milliseconds const futureTimestampMs = Date.now() + 5000; const result = await redis.pexpireat(key, futureTimestampMs); - expect(result).toBe(1); // 1 indicates success + expect(result).toBe(1); const pttl = await redis.pttl(key); expect(pttl).toBeGreaterThan(0); @@ -396,33 +343,28 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const futureTimestampMs = Date.now() + 5000; const result = await redis.pexpireat("nonexistent-pexpireat-key", futureTimestampMs); - expect(result).toBe(0); // 0 indicates key does not exist + expect(result).toBe(0); }); test("should determine the type of a key with TYPE", async () => { const redis = ctx.redis; - // String type await redis.set("string-key", "value"); const stringType = await redis.type("string-key"); expect(stringType).toBe("string"); - // List type await redis.lpush("list-key", "value"); const listType = await redis.type("list-key"); expect(listType).toBe("list"); - // Set type await redis.sadd("set-key", "value"); const setType = await redis.type("set-key"); expect(setType).toBe("set"); - // Hash type await redis.send("HSET", ["hash-key", "field", "value"]); const hashType = await redis.type("hash-key"); expect(hashType).toBe("hash"); - // Non-existent key const noneType = await redis.type("nonexistent-key"); expect(noneType).toBe("none"); }); @@ -430,19 +372,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should update last access time with TOUCH", async () => { const redis = ctx.redis; - // Set some keys await redis.set("touch-key1", "value1"); await redis.set("touch-key2", "value2"); - // Touch existing keys const touchedCount = await redis.touch("touch-key1", "touch-key2"); expect(touchedCount).toBe(2); - // Touch mix of existing and non-existing keys const mixedCount = await redis.touch("touch-key1", "nonexistent-key"); expect(mixedCount).toBe(1); - // Touch non-existent key const noneCount = await redis.touch("nonexistent-key1", "nonexistent-key2"); expect(noneCount).toBe(0); }); @@ -451,23 +389,18 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const bitKey = "mybitkey"; - // Set a bit at offset 7 to 1 const oldValue = await redis.setbit(bitKey, 7, 1); - expect(oldValue).toBe(0); // Original value was 0 + expect(oldValue).toBe(0); - // Get the bit at offset 7 const bitValue = await redis.getbit(bitKey, 7); expect(bitValue).toBe(1); - // Get a bit that wasn't set (should be 0) const unsetBit = await redis.getbit(bitKey, 100); expect(unsetBit).toBe(0); - // Set the same bit again to 0 const oldValue2 = await redis.setbit(bitKey, 7, 0); - expect(oldValue2).toBe(1); // Previous value was 1 + expect(oldValue2).toBe(1); - // Verify it's now 0 const bitValue2 = await redis.getbit(bitKey, 7); expect(bitValue2).toBe(0); }); @@ -476,12 +409,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const bitKey = "multibit"; - // Set multiple bits await redis.setbit(bitKey, 0, 1); await redis.setbit(bitKey, 3, 1); await redis.setbit(bitKey, 7, 1); - // Verify all bits expect(await redis.getbit(bitKey, 0)).toBe(1); expect(await redis.getbit(bitKey, 1)).toBe(0); expect(await redis.getbit(bitKey, 2)).toBe(0); @@ -491,7 +422,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(await redis.getbit(bitKey, 6)).toBe(0); expect(await redis.getbit(bitKey, 7)).toBe(1); - // Count the set bits const count = await redis.bitcount(bitKey); expect(count).toBe(3); }); @@ -501,19 +431,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "rangetest"; await redis.set(key, "Hello World"); - // Get substring from start to end const result1 = await redis.getrange(key, 0, 4); expect(result1).toBe("Hello"); - // Get substring with different range const result2 = await redis.getrange(key, 6, 10); expect(result2).toBe("World"); - // Get with negative offsets (count from end) const result3 = await redis.getrange(key, -5, -1); expect(result3).toBe("World"); - // Get entire string const result4 = await redis.getrange(key, 0, -1); expect(result4).toBe("Hello World"); }); @@ -523,15 +449,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "setrangetest"; await redis.set(key, "Hello World"); - // Overwrite part of the string const newLength = await redis.setrange(key, 6, "Redis"); expect(newLength).toBe(11); - // Verify the change const result = await redis.get(key); expect(result).toBe("Hello Redis"); - // Set range on non-existent key (should pad with zero bytes) const key2 = "newkey"; const newLength2 = await redis.setrange(key2, 5, "Redis"); expect(newLength2).toBeGreaterThanOrEqual(10); @@ -541,15 +464,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "append-test"; - // Append to non-existent key (creates new key) const len1 = await redis.append(key, "Hello"); expect(len1).toBe(5); - // Append to existing key const len2 = await redis.append(key, " World"); expect(len2).toBe(11); - // Verify final value const value = await redis.get(key); expect(value).toBe("Hello World"); }); @@ -557,24 +477,19 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should delete keys with DEL", async () => { const redis = ctx.redis; - // Set up test keys await redis.set("del-key1", "value1"); await redis.set("del-key2", "value2"); await redis.set("del-key3", "value3"); - // Delete single key const count1 = await redis.del("del-key1"); expect(count1).toBe(1); - // Verify key is deleted const value1 = await redis.get("del-key1"); expect(value1).toBeNull(); - // Delete multiple keys const count2 = await redis.del("del-key2", "del-key3"); expect(count2).toBe(2); - // Delete non-existent key const count3 = await redis.del("nonexistent"); expect(count3).toBe(0); }); @@ -583,15 +498,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "dump-test"; - // Set a value await redis.set(key, "test-value"); - // Dump the key const serialized = await redis.dump(key); expect(serialized).toBeDefined(); expect(serialized).not.toBeNull(); - // Dump non-existent key const empty = await redis.dump("nonexistent"); expect(empty).toBeNull(); }); @@ -600,15 +512,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getbuffer-test"; - // Set a value await redis.set(key, "test-value"); - // Get as Buffer const buffer = await redis.getBuffer(key); expect(buffer).toBeInstanceOf(Buffer); expect(buffer?.toString()).toBe("test-value"); - // Non-existent key const empty = await redis.getBuffer("nonexistent"); expect(empty).toBeNull(); }); @@ -617,18 +526,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getdel-test"; - // Set a value await redis.set(key, "test-value"); - // Get and delete const value = await redis.getdel(key); expect(value).toBe("test-value"); - // Verify key is deleted const deleted = await redis.get(key); expect(deleted).toBeNull(); - // Non-existent key const empty = await redis.getdel("nonexistent"); expect(empty).toBeNull(); }); @@ -637,24 +542,20 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getex-test"; - // Set a value await redis.set(key, "test-value"); - // Get with EX (seconds) const value1 = await redis.getex(key, "EX", 60); expect(value1).toBe("test-value"); const ttl1 = await redis.ttl(key); expect(ttl1).toBeGreaterThan(0); expect(ttl1).toBeLessThanOrEqual(60); - // Get with PX (milliseconds) const value2 = await redis.getex(key, "PX", 5000); expect(value2).toBe("test-value"); const pttl = await redis.pttl(key); expect(pttl).toBeGreaterThan(0); expect(pttl).toBeLessThanOrEqual(5000); - // Non-existent key const empty = await redis.getex("nonexistent", "EX", 60); expect(empty).toBeNull(); }); @@ -663,15 +564,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getset-test"; - // GETSET on non-existent key returns null const old1 = await redis.getset(key, "value1"); expect(old1).toBeNull(); - // GETSET on existing key returns old value const old2 = await redis.getset(key, "value2"); expect(old2).toBe("value1"); - // Verify new value is set const current = await redis.get(key); expect(current).toBe("value2"); }); @@ -680,11 +578,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "strlen-test"; - // Non-existent key returns 0 const len1 = await redis.strlen(key); expect(len1).toBe(0); - // Set values and check lengths await redis.set(key, "Hello"); const len2 = await redis.strlen(key); expect(len2).toBe(5); @@ -700,7 +596,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.set(key, "Hello World"); - // SUBSTR is deprecated alias for GETRANGE const result = await redis.substr(key, 0, 4); expect(result).toBe("Hello"); }); @@ -711,22 +606,18 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.set(key, "value"); - // Set expiration const futureTs = Math.floor(Date.now() / 1000) + 60; await redis.expireat(key, futureTs); - // Get expiration time const expireTime = await redis.expiretime(key); expect(expireTime).toBeGreaterThan(0); expect(expireTime).toBeLessThanOrEqual(futureTs); - // Key with no expiration returns -1 const key2 = "no-expire"; await redis.set(key2, "value"); const noExpire = await redis.expiretime(key2); expect(noExpire).toBe(-1); - // Non-existent key returns -2 const nonExist = await redis.expiretime("nonexistent"); expect(nonExist).toBe(-2); }); @@ -737,22 +628,18 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.set(key, "value"); - // Set expiration const futureTs = Date.now() + 5000; await redis.pexpireat(key, futureTs); - // Get expiration time const pexpireTime = await redis.pexpiretime(key); expect(pexpireTime).toBeGreaterThan(0); expect(pexpireTime).toBeLessThanOrEqual(futureTs); - // Key with no expiration returns -1 const key2 = "no-expire"; await redis.set(key2, "value"); const noExpire = await redis.pexpiretime(key2); expect(noExpire).toBe(-1); - // Non-existent key returns -2 const nonExist = await redis.pexpiretime("nonexistent"); expect(nonExist).toBe(-2); }); @@ -764,23 +651,18 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.set(key, "value"); await redis.expire(key, 60); - // Verify expiration is set const ttlBefore = await redis.ttl(key); expect(ttlBefore).toBeGreaterThan(0); - // Remove expiration const result = await redis.persist(key); expect(result).toBe(1); - // Verify no expiration const ttlAfter = await redis.ttl(key); expect(ttlAfter).toBe(-1); - // Persist on key without expiration returns 0 const result2 = await redis.persist(key); expect(result2).toBe(0); - // Persist on non-existent key returns 0 const result3 = await redis.persist("nonexistent"); expect(result3).toBe(0); }); @@ -788,20 +670,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should get multiple values with MGET", async () => { const redis = ctx.redis; - // Set multiple keys await redis.set("mget-key1", "value1"); await redis.set("mget-key2", "value2"); await redis.set("mget-key3", "value3"); - // Get multiple keys const values = await redis.mget("mget-key1", "mget-key2", "mget-key3"); expect(values).toEqual(["value1", "value2", "value3"]); - // Mix of existing and non-existing keys const mixed = await redis.mget("mget-key1", "nonexistent", "mget-key2"); expect(mixed).toEqual(["value1", null, "value2"]); - // All non-existent const allNull = await redis.mget("none1", "none2", "none3"); expect(allNull).toEqual([null, null, null]); }); @@ -810,18 +688,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "setnx-test"; - // First SETNX succeeds const result1 = await redis.setnx(key, "value1"); expect(result1).toBe(1); const value1 = await redis.get(key); expect(value1).toBe("value1"); - // Second SETNX fails const result2 = await redis.setnx(key, "value2"); expect(result2).toBe(0); - // Value unchanged const value2 = await redis.get(key); expect(value2).toBe("value1"); }); @@ -830,42 +705,36 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "pfadd-test"; - // Add first element const result1 = await redis.pfadd(key, "element1"); expect(result1).toBe(1); - // Add different element const result2 = await redis.pfadd(key, "element2"); expect(result2).toBe(1); - // Add same element (HLL not modified) const result3 = await redis.pfadd(key, "element1"); expect(result3).toBe(0); }); test("should implement TTL command correctly for different cases", async () => { const redis = ctx.redis; - // 1. Key with expiration + const tempKey = "ttl-test-key"; await redis.set(tempKey, "ttl test value"); await redis.expire(tempKey, 60); - // Use native ttl command const ttl = await redis.ttl(tempKey); expectType(ttl, "number"); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(60); - // 2. Key with no expiration const permanentKey = "permanent-key"; await redis.set(permanentKey, "no expiry"); const noExpiry = await redis.ttl(permanentKey); - expect(noExpiry).toMatchInlineSnapshot(`-1`); // -1 indicates no expiration + expect(noExpiry).toMatchInlineSnapshot(`-1`); - // 3. Non-existent key const nonExistentKey = "non-existent-" + randomUUIDv7(); const noKey = await redis.ttl(nonExistentKey); - expect(noKey).toMatchInlineSnapshot(`-2`); // -2 indicates key doesn't exist + expect(noKey).toMatchInlineSnapshot(`-2`); }); test("should copy a key to a new key with COPY", async () => { @@ -873,22 +742,18 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const sourceKey = "copy-source"; const destKey = "copy-dest"; - // Set source key await redis.set(sourceKey, "Hello World"); - // Copy to destination const result = await redis.copy(sourceKey, destKey); - expect(result).toBe(1); // 1 indicates successful copy + expect(result).toBe(1); - // Verify both keys exist with same value const sourceValue = await redis.get(sourceKey); const destValue = await redis.get(destKey); expect(sourceValue).toBe("Hello World"); expect(destValue).toBe("Hello World"); - // Trying to copy to an existing key without REPLACE should fail const result2 = await redis.copy(sourceKey, destKey); - expect(result2).toBe(0); // 0 indicates copy failed + expect(result2).toBe(0); }); test("should copy a key with REPLACE option", async () => { @@ -896,15 +761,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const sourceKey = "copy-replace-source"; const destKey = "copy-replace-dest"; - // Set both keys await redis.set(sourceKey, "New Value"); await redis.set(destKey, "Old Value"); - // Copy with REPLACE const result = await redis.copy(sourceKey, destKey, "REPLACE"); expect(result).toBe(1); - // Verify destination was replaced const destValue = await redis.get(destKey); expect(destValue).toBe("New Value"); }); @@ -912,16 +774,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should unlink one or more keys asynchronously with UNLINK", async () => { const redis = ctx.redis; - // Set multiple keys await redis.set("unlink-key1", "value1"); await redis.set("unlink-key2", "value2"); await redis.set("unlink-key3", "value3"); - // Unlink multiple keys const result = await redis.unlink("unlink-key1", "unlink-key2", "unlink-key3"); - expect(result).toBe(3); // All 3 keys were unlinked + expect(result).toBe(3); - // Verify keys are gone expect(await redis.get("unlink-key1")).toBeNull(); expect(await redis.get("unlink-key2")).toBeNull(); expect(await redis.get("unlink-key3")).toBeNull(); @@ -930,36 +789,29 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should unlink with non-existent keys", async () => { const redis = ctx.redis; - // Set one key await redis.set("unlink-exists", "value"); - // Try to unlink mix of existing and non-existing keys const result = await redis.unlink("unlink-exists", "unlink-nonexist1", "unlink-nonexist2"); - expect(result).toBe(1); // Only 1 key existed and was unlinked + expect(result).toBe(1); - // Verify key is gone expect(await redis.get("unlink-exists")).toBeNull(); }); test("should return a random key with RANDOMKEY", async () => { const redis = ctx.redis; - // Empty database should return null const emptyResult = await redis.randomkey(); expect(emptyResult).toBeNull(); - // Set multiple keys await redis.set("random-key1", "value1"); await redis.set("random-key2", "value2"); await redis.set("random-key3", "value3"); - // Get a random key const randomKey = await redis.randomkey(); expect(randomKey).toBeDefined(); expect(randomKey).not.toBeNull(); expect(["random-key1", "random-key2", "random-key3"]).toContain(randomKey); - // Verify the key exists const value = await redis.get(randomKey!); expect(value).toBeDefined(); }); @@ -967,13 +819,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should iterate keys with SCAN", async () => { const redis = ctx.redis; - // Set multiple keys with a pattern const testKeys = ["scan-test:1", "scan-test:2", "scan-test:3", "scan-test:4", "scan-test:5"]; for (const key of testKeys) { await redis.set(key, "value"); } - // Scan all keys let cursor = "0"; const foundKeys: string[] = []; do { @@ -982,7 +832,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { cursor = nextCursor; } while (cursor !== "0"); - // Verify all test keys were found for (const testKey of testKeys) { expect(foundKeys).toContain(testKey); } @@ -991,13 +840,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should iterate keys with SCAN and MATCH pattern", async () => { const redis = ctx.redis; - // Set keys with different patterns await redis.set("user:1", "alice"); await redis.set("user:2", "bob"); await redis.set("post:1", "hello"); await redis.set("post:2", "world"); - // Scan with MATCH pattern let cursor = "0"; const userKeys: string[] = []; do { @@ -1006,7 +853,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { cursor = nextCursor; } while (cursor !== "0"); - // Should only find user keys expect(userKeys).toContain("user:1"); expect(userKeys).toContain("user:2"); expect(userKeys).not.toContain("post:1"); @@ -1181,14 +1027,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "append-test"; - // Append to non-existent key (should create it) const length1 = await redis.append(key, "Hello"); expect(length1).toBe(5); const value1 = await redis.get(key); expect(value1).toBe("Hello"); - // Append to existing key const length2 = await redis.append(key, " World"); expect(length2).toBe(11); @@ -1199,27 +1043,21 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should delete one or more keys with DEL", async () => { const redis = ctx.redis; - // Set multiple keys await redis.set("del-test-1", "value1"); await redis.set("del-test-2", "value2"); await redis.set("del-test-3", "value3"); - // Delete single key const result1 = await redis.del("del-test-1"); expect(result1).toBe(1); - // Verify key was deleted expect(await redis.get("del-test-1")).toBeNull(); - // Delete multiple keys const result2 = await redis.del("del-test-2", "del-test-3"); expect(result2).toBe(2); - // Verify keys were deleted expect(await redis.get("del-test-2")).toBeNull(); expect(await redis.get("del-test-3")).toBeNull(); - // Delete non-existent key const result3 = await redis.del("del-test-nonexistent"); expect(result3).toBe(0); }); @@ -1228,18 +1066,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "dump-test"; - // Set a value await redis.set(key, "Hello World"); - // Dump the key const serialized = await redis.dump(key); expect(serialized).toBeDefined(); expect(serialized).not.toBeNull(); - // DUMP returns binary data, so it should be a Buffer or string expect(typeof serialized === "string" || Buffer.isBuffer(serialized)).toBe(true); - // Dump non-existent key should return null const nonExistent = await redis.dump("dump-test-nonexistent"); expect(nonExistent).toBeNull(); }); @@ -1248,17 +1082,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getbuffer-test"; - // Set a value await redis.set(key, "Hello Buffer"); - // Get as Buffer const buffer = await redis.getBuffer(key); expect(buffer).toBeDefined(); expect(buffer).not.toBeNull(); expect(Buffer.isBuffer(buffer)).toBe(true); expect(buffer!.toString()).toBe("Hello Buffer"); - // Get non-existent key const nonExistent = await redis.getBuffer("getbuffer-nonexistent"); expect(nonExistent).toBeNull(); }); @@ -1267,18 +1098,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getdel-test"; - // Set a value await redis.set(key, "Delete me"); - // Get and delete const value = await redis.getdel(key); expect(value).toBe("Delete me"); - // Verify key was deleted const check = await redis.get(key); expect(check).toBeNull(); - // GETDEL on non-existent key const nonExistent = await redis.getdel("getdel-nonexistent"); expect(nonExistent).toBeNull(); }); @@ -1287,18 +1114,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getex-ex-test"; - // Set a value await redis.set(key, "Expire me"); - // Get with EX option (seconds) const value = await redis.getex(key, "EX", 10); expect(value).toBe("Expire me"); - // Verify key still exists const check = await redis.get(key); expect(check).toBe("Expire me"); - // Verify TTL was set const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(10); @@ -1308,14 +1131,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getex-px-test"; - // Set a value await redis.set(key, "Expire me"); - // Get with PX option (milliseconds) const value = await redis.getex(key, "PX", 5000); expect(value).toBe("Expire me"); - // Verify TTL was set (in seconds) const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(5); @@ -1325,15 +1145,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getex-exat-test"; - // Set a value await redis.set(key, "Expire at timestamp"); - // Get with EXAT option (unix timestamp in seconds) const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const value = await redis.getex(key, "EXAT", futureTimestamp); expect(value).toBe("Expire at timestamp"); - // Verify TTL was set const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(60); @@ -1343,15 +1160,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getex-pxat-test"; - // Set a value await redis.set(key, "Expire at timestamp"); - // Get with PXAT option (unix timestamp in milliseconds) const futureTimestamp = Date.now() + 60000; const value = await redis.getex(key, "PXAT", futureTimestamp); expect(value).toBe("Expire at timestamp"); - // Verify TTL was set const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(60); @@ -1361,20 +1175,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getex-persist-test"; - // Set a value with expiration await redis.set(key, "Remove expiration", "EX", 100); - // Verify it has expiration const ttlBefore = await redis.ttl(key); expect(ttlBefore).toBeGreaterThan(0); - // Get with PERSIST option const value = await redis.getex(key, "PERSIST"); expect(value).toBe("Remove expiration"); - // Verify expiration was removed const ttlAfter = await redis.ttl(key); - expect(ttlAfter).toBe(-1); // -1 means no expiration + expect(ttlAfter).toBe(-1); }); test("should get non-existent key with GETEX", async () => { @@ -1388,19 +1198,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "getset-test"; - // GETSET on non-existent key returns null const oldValue1 = await redis.getset(key, "new value"); expect(oldValue1).toBeNull(); - // Verify new value was set const check1 = await redis.get(key); expect(check1).toBe("new value"); - // GETSET on existing key returns old value const oldValue2 = await redis.getset(key, "newer value"); expect(oldValue2).toBe("new value"); - // Verify new value was set const check2 = await redis.get(key); expect(check2).toBe("newer value"); }); @@ -1409,16 +1215,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "strlen-test"; - // STRLEN on non-existent key const length1 = await redis.strlen("strlen-nonexistent"); expect(length1).toBe(0); - // Set a value and get its length await redis.set(key, "Hello World"); const length2 = await redis.strlen(key); expect(length2).toBe(11); - // Test with different value await redis.set(key, "Hi"); const length3 = await redis.strlen(key); expect(length3).toBe(2); @@ -1430,18 +1233,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "llen-test"; - // Empty list should return 0 const len1 = await redis.llen(key); expect(len1).toBe(0); - // Add elements await redis.lpush(key, "one", "two", "three"); - // Get length const len2 = await redis.llen(key); expect(len2).toBe(3); - // Pop one element await redis.lpop(key); const len3 = await redis.llen(key); expect(len3).toBe(2); @@ -1451,18 +1250,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lpop-test"; - // Add elements await redis.lpush(key, "three", "two", "one"); - // Pop one element const elem1 = await redis.lpop(key); expect(elem1).toBe("one"); - // Pop with count const elem2 = await redis.lpop(key, 2); expect(elem2).toEqual(["two", "three"]); - // Pop from empty list const empty = await redis.lpop(key); expect(empty).toBeNull(); }); @@ -1471,18 +1266,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lpushx-test"; - // LPUSHX on non-existent key should return 0 const len1 = await redis.lpushx(key, "value"); expect(len1).toBe(0); - // Create the list first await redis.lpush(key, "one"); - // Now LPUSHX should work const len2 = await redis.lpushx(key, "two"); expect(len2).toBe(2); - // Verify order const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["two", "one"]); }); @@ -1507,18 +1298,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "rpushx-test"; - // RPUSHX on non-existent key should return 0 const len1 = await redis.rpushx(key, "value"); expect(len1).toBe(0); - // Create the list first await redis.rpush(key, "one"); - // Now RPUSHX should work const len2 = await redis.rpushx(key, "two"); expect(len2).toBe(2); - // Verify order const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["one", "two"]); }); @@ -1527,32 +1314,25 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lrange-test"; - // Create a list with multiple elements await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); - // Get full list const fullList = await redis.lrange(key, 0, -1); expect(fullList).toEqual(["one", "two", "three"]); - // Get first two elements const firstTwo = await redis.lrange(key, 0, 1); expect(firstTwo).toEqual(["one", "two"]); - // Get last two elements using negative indexes const lastTwo = await redis.lrange(key, -2, -1); expect(lastTwo).toEqual(["two", "three"]); - // Get middle element const middle = await redis.lrange(key, 1, 1); expect(middle).toEqual(["two"]); - // Out of range should return empty array const outOfRange = await redis.lrange(key, 10, 20); expect(outOfRange).toEqual([]); - // Non-existent key should return empty array const nonExistent = await redis.lrange("nonexistent-list", 0, -1); expect(nonExistent).toEqual([]); }); @@ -1561,12 +1341,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lindex-test"; - // Create a list await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); - // Get element at positive index const first = await redis.lindex(key, 0); expect(first).toBe("one"); @@ -1576,21 +1354,18 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const third = await redis.lindex(key, 2); expect(third).toBe("three"); - // Get element at negative index (counting from end) const last = await redis.lindex(key, -1); expect(last).toBe("three"); const secondLast = await redis.lindex(key, -2); expect(secondLast).toBe("two"); - // Out of range should return null const outOfRange = await redis.lindex(key, 10); expect(outOfRange).toBeNull(); const outOfRangeNeg = await redis.lindex(key, -10); expect(outOfRangeNeg).toBeNull(); - // Non-existent key should return null const nonExistent = await redis.lindex("nonexistent-list", 0); expect(nonExistent).toBeNull(); }); @@ -1599,28 +1374,22 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lset-test"; - // Create a list await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); - // Set element at positive index const result1 = await redis.lset(key, 0, "zero"); expect(result1).toBe("OK"); - // Verify the change const first = await redis.lindex(key, 0); expect(first).toBe("zero"); - // Set element at negative index const result2 = await redis.lset(key, -1, "last"); expect(result2).toBe("OK"); - // Verify the change const last = await redis.lindex(key, -1); expect(last).toBe("last"); - // Check full list const fullList = await redis.lrange(key, 0, -1); expect(fullList).toEqual(["zero", "two", "last"]); }); @@ -1634,12 +1403,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.lset("lset-error-test", 10, "newvalue"); }).toThrow(/index out of range/i); - // Non-existent key should throw an error expect(async () => { await redis.lset("nonexistent-list", 0, "value"); }).toThrow(/no such key/i); - // Wrong type (not a list) should throw an error await redis.set("string-key", "value"); expect(async () => { await redis.lset("string-key", 0, "value"); @@ -1650,24 +1417,19 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lrange-advanced"; - // Create a longer list for (let i = 5; i >= 1; i--) { await redis.lpush(key, String(i)); } - // Verify the list: [1, 2, 3, 4, 5] const fullList = await redis.lrange(key, 0, -1); expect(fullList).toEqual(["1", "2", "3", "4", "5"]); - // Test with stop less than start (should return empty) const invalid = await redis.lrange(key, 3, 1); expect(invalid).toEqual([]); - // Test with negative start and positive stop const mixed = await redis.lrange(key, -3, 4); expect(mixed).toEqual(["3", "4", "5"]); - // Test with both negative const bothNeg = await redis.lrange(key, -4, -2); expect(bothNeg).toEqual(["2", "3", "4"]); }); @@ -1676,16 +1438,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "list-numbers"; - // Push numeric strings await redis.lpush(key, "100"); await redis.lpush(key, "200"); await redis.lpush(key, "300"); - // Get element const elem = await redis.lindex(key, 1); expect(elem).toBe("200"); - // Set with number (should convert to string) await redis.lset(key, 1, "250"); const updated = await redis.lindex(key, 1); expect(updated).toBe("250"); @@ -1695,15 +1454,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "linsert-before-test"; - // Create a list await redis.lpush(key, "World"); await redis.lpush(key, "Hello"); - // Insert before "World" const result = await redis.linsert(key, "BEFORE", "World", "There"); - expect(result).toBe(3); // List length after insert + expect(result).toBe(3); - // Verify the list const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["Hello", "There", "World"]); }); @@ -1712,15 +1468,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "linsert-after-test"; - // Create a list await redis.lpush(key, "World"); await redis.lpush(key, "Hello"); - // Insert after "Hello" const result = await redis.linsert(key, "AFTER", "Hello", "Beautiful"); - expect(result).toBe(3); // List length after insert + expect(result).toBe(3); - // Verify the list const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["Hello", "Beautiful", "World"]); }); @@ -1729,15 +1482,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "linsert-notfound-test"; - // Create a list await redis.lpush(key, "value1"); await redis.lpush(key, "value2"); - // Try to insert before non-existent pivot const result = await redis.linsert(key, "BEFORE", "nonexistent", "newvalue"); - expect(result).toBe(-1); // Pivot not found + expect(result).toBe(-1); - // Verify list unchanged const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["value2", "value1"]); }); @@ -1745,26 +1495,22 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should handle LINSERT on non-existent key", async () => { const redis = ctx.redis; - // Try to insert into non-existent list const result = await redis.linsert("nonexistent-list", "BEFORE", "pivot", "element"); - expect(result).toBe(0); // Key doesn't exist + expect(result).toBe(0); }); test("should remove elements from head with LREM", async () => { const redis = ctx.redis; const key = "lrem-positive-test"; - // Create a list with duplicates await redis.rpush(key, "hello"); await redis.rpush(key, "hello"); await redis.rpush(key, "world"); await redis.rpush(key, "hello"); - // Remove first 2 "hello" from head to tail const result = await redis.lrem(key, 2, "hello"); - expect(result).toBe(2); // Number of elements removed + expect(result).toBe(2); - // Verify the list const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["world", "hello"]); }); @@ -1773,17 +1519,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lrem-negative-test"; - // Create a list with duplicates await redis.rpush(key, "hello"); await redis.rpush(key, "world"); await redis.rpush(key, "hello"); await redis.rpush(key, "hello"); - // Remove first 2 "hello" from tail to head const result = await redis.lrem(key, -2, "hello"); - expect(result).toBe(2); // Number of elements removed + expect(result).toBe(2); - // Verify the list const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["hello", "world"]); }); @@ -1792,18 +1535,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lrem-all-test"; - // Create a list with multiple duplicates await redis.rpush(key, "hello"); await redis.rpush(key, "world"); await redis.rpush(key, "hello"); await redis.rpush(key, "foo"); await redis.rpush(key, "hello"); - // Remove all "hello" const result = await redis.lrem(key, 0, "hello"); - expect(result).toBe(3); // Number of elements removed + expect(result).toBe(3); - // Verify the list const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["world", "foo"]); }); @@ -1812,15 +1552,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lrem-notfound-test"; - // Create a list await redis.rpush(key, "value1"); await redis.rpush(key, "value2"); - // Try to remove non-existent element const result = await redis.lrem(key, 1, "nonexistent"); - expect(result).toBe(0); // No elements removed + expect(result).toBe(0); - // Verify list unchanged const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["value1", "value2"]); }); @@ -1829,17 +1566,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "ltrim-test"; - // Create a list await redis.rpush(key, "one"); await redis.rpush(key, "two"); await redis.rpush(key, "three"); await redis.rpush(key, "four"); - // Trim to keep only elements 1-2 const result = await redis.ltrim(key, 1, 2); expect(result).toBe("OK"); - // Verify the list const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["two", "three"]); }); @@ -1848,18 +1582,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "ltrim-negative-test"; - // Create a list await redis.rpush(key, "one"); await redis.rpush(key, "two"); await redis.rpush(key, "three"); await redis.rpush(key, "four"); await redis.rpush(key, "five"); - // Trim to keep last 3 elements const result = await redis.ltrim(key, -3, -1); expect(result).toBe("OK"); - // Verify the list const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["three", "four", "five"]); }); @@ -1868,16 +1599,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "ltrim-outofrange-test"; - // Create a list await redis.rpush(key, "one"); await redis.rpush(key, "two"); await redis.rpush(key, "three"); - // Trim with large range (should keep all elements) const result = await redis.ltrim(key, 0, 100); expect(result).toBe("OK"); - // Verify the list unchanged const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["one", "two", "three"]); }); @@ -1886,16 +1614,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "ltrim-empty-test"; - // Create a list await redis.rpush(key, "one"); await redis.rpush(key, "two"); await redis.rpush(key, "three"); - // Trim with invalid range (stop < start) const result = await redis.ltrim(key, 2, 0); expect(result).toBe("OK"); - // Verify the list is empty const list = await redis.lrange(key, 0, -1); expect(list).toEqual([]); }); @@ -1904,14 +1629,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "blpop-test"; - // Push an element await redis.lpush(key, "value1"); - // BLPOP should return immediately const result = await redis.blpop(key, 0.1); expect(result).toEqual([key, "value1"]); - // BLPOP on empty list should timeout and return null const timeout = await redis.blpop(key, 0.1); expect(timeout).toBeNull(); }); @@ -1920,16 +1642,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "brpop-test"; - // Push multiple elements await redis.lpush(key, "value2"); await redis.lpush(key, "value1"); - // BRPOP should return last element const result = await redis.brpop(key, 0.1); expect(result).toEqual([key, "value2"]); - // BRPOP on empty list should timeout and return null - await redis.brpop(key, 0.1); // Pop remaining element + await redis.brpop(key, 0.1); const timeout = await redis.brpop(key, 0.1); expect(timeout).toBeNull(); }); @@ -1939,10 +1658,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "blpop-list1"; const key2 = "blpop-list2"; - // Only push to second list await redis.lpush(key2, "value2"); - // Should pop from key2 since key1 is empty const result = await redis.blpop(key1, key2, 0.1); expect(result).toEqual([key2, "value2"]); }); @@ -1951,16 +1668,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lmpop-left-test"; - // Push elements await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); - // Pop one element from head (LEFT) const result = await redis.lmpop(1, key, "LEFT"); expect(result).toEqual([key, ["one"]]); - // Verify remaining elements const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual(["two", "three"]); }); @@ -1969,16 +1683,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lmpop-right-test"; - // Push elements await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); - // Pop one element from tail (RIGHT) const result = await redis.lmpop(1, key, "RIGHT"); expect(result).toEqual([key, ["three"]]); - // Verify remaining elements const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual(["one", "two"]); }); @@ -1987,16 +1698,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lmpop-count-test"; - // Push elements await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); - // Pop 2 elements from head const result = await redis.lmpop(1, key, "LEFT", "COUNT", 2); expect(result).toEqual([key, ["one", "two"]]); - // Verify remaining element const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual(["three"]); }); @@ -2004,7 +1712,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should return null for LMPOP on empty list", async () => { const redis = ctx.redis; - // Try to pop from non-existent list const result = await redis.lmpop(1, "nonexistent-list", "LEFT"); expect(result).toBeNull(); }); @@ -2014,10 +1721,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "lmpop-empty"; const key2 = "lmpop-full"; - // Only push to second list await redis.lpush(key2, "value"); - // Should pop from key2 since key1 is empty const result = await redis.lmpop(2, key1, key2, "LEFT"); expect(result).toEqual([key2, ["value"]]); }); @@ -2026,27 +1731,21 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lpos-test"; - // Create a list with some duplicates await redis.lpush(key, "d"); await redis.lpush(key, "b"); await redis.lpush(key, "c"); await redis.lpush(key, "b"); await redis.lpush(key, "a"); - // List is now: ["a", "b", "c", "b", "d"] - // Find first occurrence of "b" const pos1 = await redis.lpos(key, "b"); expect(pos1).toBe(1); - // Find first occurrence of "a" const pos2 = await redis.lpos(key, "a"); expect(pos2).toBe(0); - // Find element at the end const pos3 = await redis.lpos(key, "d"); expect(pos3).toBe(4); - // Non-existent element should return null const pos4 = await redis.lpos(key, "x"); expect(pos4).toBeNull(); }); @@ -2055,31 +1754,24 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lpos-rank-test"; - // Create a list with duplicates await redis.lpush(key, "b"); await redis.lpush(key, "a"); await redis.lpush(key, "b"); await redis.lpush(key, "a"); await redis.lpush(key, "b"); - // List is now: ["b", "a", "b", "a", "b"] - // Find first occurrence (default behavior) const first = await redis.lpos(key, "b"); expect(first).toBe(0); - // Find second occurrence const second = await redis.lpos(key, "b", "RANK", 2); expect(second).toBe(2); - // Find third occurrence const third = await redis.lpos(key, "b", "RANK", 3); expect(third).toBe(4); - // Find with RANK that doesn't exist const fourth = await redis.lpos(key, "b", "RANK", 4); expect(fourth).toBeNull(); - // Find with negative RANK (from tail) const fromEnd = await redis.lpos(key, "b", "RANK", -1); expect(fromEnd).toBe(4); @@ -2091,27 +1783,21 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lpos-count-test"; - // Create a list with duplicates await redis.lpush(key, "c"); await redis.lpush(key, "b"); await redis.lpush(key, "b"); await redis.lpush(key, "a"); await redis.lpush(key, "b"); - // List is now: ["b", "a", "b", "b", "c"] - // Find all occurrences (COUNT 0 means all) const all = await redis.lpos(key, "b", "COUNT", 0); expect(all).toEqual([0, 2, 3]); - // Find first 2 occurrences const first2 = await redis.lpos(key, "b", "COUNT", 2); expect(first2).toEqual([0, 2]); - // Find with COUNT greater than actual occurrences const more = await redis.lpos(key, "b", "COUNT", 10); expect(more).toEqual([0, 2, 3]); - // Find non-existent with COUNT const none = await redis.lpos(key, "x", "COUNT", 5); expect(none).toEqual([]); }); @@ -2120,22 +1806,17 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lpos-maxlen-test"; - // Create a longer list for (let i = 5; i >= 1; i--) { await redis.lpush(key, String(i)); } await redis.lpush(key, "target"); - // List is now: ["target", "1", "2", "3", "4", "5"] - // Find within first 6 elements (should find it) const found = await redis.lpos(key, "target", "MAXLEN", 6); expect(found).toBe(0); - // Find "5" with MAXLEN that's too short const notFound = await redis.lpos(key, "5", "MAXLEN", 3); expect(notFound).toBeNull(); - // Find "3" with MAXLEN const found3 = await redis.lpos(key, "3", "MAXLEN", 10); expect(found3).toBe(3); }); @@ -2145,24 +1826,19 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const source = "lmove-source"; const dest = "lmove-dest"; - // Setup source list await redis.lpush(source, "three"); await redis.lpush(source, "two"); await redis.lpush(source, "one"); - // Source: ["one", "two", "three"] - // Move from LEFT of source to RIGHT of dest const result1 = await redis.lmove(source, dest, "LEFT", "RIGHT"); expect(result1).toBe("one"); - // Verify source and dest const sourceList1 = await redis.lrange(source, 0, -1); expect(sourceList1).toEqual(["two", "three"]); const destList1 = await redis.lrange(dest, 0, -1); expect(destList1).toEqual(["one"]); - // Move from RIGHT of source to LEFT of dest const result2 = await redis.lmove(source, dest, "RIGHT", "LEFT"); expect(result2).toBe("three"); @@ -2176,25 +1852,21 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should handle all LMOVE direction combinations", async () => { const redis = ctx.redis; - // Test LEFT -> LEFT await redis.lpush("src1", "b", "a"); const res1 = await redis.lmove("src1", "dst1", "LEFT", "LEFT"); expect(res1).toBe("a"); expect(await redis.lrange("dst1", 0, -1)).toEqual(["a"]); - // Test LEFT -> RIGHT await redis.lpush("src2", "b", "a"); const res2 = await redis.lmove("src2", "dst2", "LEFT", "RIGHT"); expect(res2).toBe("a"); expect(await redis.lrange("dst2", 0, -1)).toEqual(["a"]); - // Test RIGHT -> LEFT await redis.lpush("src3", "b", "a"); const res3 = await redis.lmove("src3", "dst3", "RIGHT", "LEFT"); expect(res3).toBe("b"); expect(await redis.lrange("dst3", 0, -1)).toEqual(["b"]); - // Test RIGHT -> RIGHT await redis.lpush("src4", "b", "a"); const res4 = await redis.lmove("src4", "dst4", "RIGHT", "RIGHT"); expect(res4).toBe("b"); @@ -2207,7 +1879,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const result = await redis.lmove("empty-source", "some-dest", "LEFT", "RIGHT"); expect(result).toBeNull(); - // Destination should also be empty const destList = await redis.lrange("some-dest", 0, -1); expect(destList).toEqual([]); }); @@ -2217,9 +1888,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "circular-list"; await redis.lpush(key, "c", "b", "a"); - // List: ["a", "b", "c"] - // Move from LEFT to RIGHT (rotate left) const result = await redis.lmove(key, key, "LEFT", "RIGHT"); expect(result).toBe("a"); expect(await redis.lrange(key, 0, -1)).toEqual(["b", "c", "a"]); @@ -2230,24 +1899,19 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const source = "rpoplpush-source"; const dest = "rpoplpush-dest"; - // Setup source list await redis.lpush(source, "three"); await redis.lpush(source, "two"); await redis.lpush(source, "one"); - // Source: ["one", "two", "three"] - // Pop from tail (RIGHT) of source and push to head (LEFT) of dest const result = await redis.rpoplpush(source, dest); expect(result).toBe("three"); - // Verify source and dest const sourceList = await redis.lrange(source, 0, -1); expect(sourceList).toEqual(["one", "two"]); const destList = await redis.lrange(dest, 0, -1); expect(destList).toEqual(["three"]); - // Do it again const result2 = await redis.rpoplpush(source, dest); expect(result2).toBe("two"); @@ -2270,9 +1934,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "circular-rpoplpush"; await redis.lpush(key, "c", "b", "a"); - // List: ["a", "b", "c"] - // Pop from tail and push to head (rotate right) const result = await redis.rpoplpush(key, key); expect(result).toBe("c"); expect(await redis.lrange(key, 0, -1)).toEqual(["c", "a", "b"]); @@ -2283,28 +1945,22 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const source = "blmove-source"; const dest = "blmove-dest"; - // Push elements to source await redis.lpush(source, "three"); await redis.lpush(source, "two"); await redis.lpush(source, "one"); - // Move from right of source to left of dest (like BRPOPLPUSH) const result = await redis.blmove(source, dest, "RIGHT", "LEFT", 0.1); expect(result).toBe("three"); - // Verify source has 2 elements const sourceRemaining = await redis.lrange(source, 0, -1); expect(sourceRemaining).toEqual(["one", "two"]); - // Verify dest has 1 element at head const destElements = await redis.lrange(dest, 0, -1); expect(destElements).toEqual(["three"]); - // Move from left of source to right of dest const result2 = await redis.blmove(source, dest, "LEFT", "RIGHT", 0.1); expect(result2).toBe("one"); - // Verify final state const finalSource = await redis.lrange(source, 0, -1); expect(finalSource).toEqual(["two"]); @@ -2315,7 +1971,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should timeout and return null with BLMOVE on empty list", async () => { const redis = ctx.redis; - // Try to move from empty list with short timeout const result = await redis.blmove("empty-source", "dest", "LEFT", "RIGHT", 0.1); expect(result).toBeNull(); }); @@ -2324,20 +1979,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "blmpop-test"; - // Push elements await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); - // Pop one element from head (LEFT) const result = await redis.blmpop(0.1, 1, key, "LEFT"); expect(result).toEqual([key, ["one"]]); - // Pop 2 elements from tail (RIGHT) with COUNT const result2 = await redis.blmpop(0.1, 1, key, "RIGHT", "COUNT", 2); expect(result2).toEqual([key, ["three", "two"]]); - // List should now be empty const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual([]); }); @@ -2347,10 +1998,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "blmpop-empty"; const key2 = "blmpop-full"; - // Only push to second list await redis.lpush(key2, "value"); - // Should pop from key2 since key1 is empty const result = await redis.blmpop(0.1, 2, key1, key2, "LEFT"); expect(result).toEqual([key2, ["value"]]); }); @@ -2358,7 +2007,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should timeout and return null with BLMPOP on empty lists", async () => { const redis = ctx.redis; - // Try to pop from non-existent lists with short timeout const result = await redis.blmpop(0.1, 2, "empty-list1", "empty-list2", "LEFT"); expect(result).toBeNull(); }); @@ -2368,31 +2016,24 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const source = "brpoplpush-source"; const dest = "brpoplpush-dest"; - // Push elements to source await redis.lpush(source, "value2"); await redis.lpush(source, "value1"); - // Move from tail of source to head of dest const result = await redis.brpoplpush(source, dest, 0.1); expect(result).toBe("value2"); - // Verify source has 1 element const sourceRemaining = await redis.lrange(source, 0, -1); expect(sourceRemaining).toEqual(["value1"]); - // Verify dest has 1 element const destElements = await redis.lrange(dest, 0, -1); expect(destElements).toEqual(["value2"]); - // Move again const result2 = await redis.brpoplpush(source, dest, 0.1); expect(result2).toBe("value1"); - // Source should be empty const finalSource = await redis.lrange(source, 0, -1); expect(finalSource).toEqual([]); - // Dest should have both elements const finalDest = await redis.lrange(dest, 0, -1); expect(finalDest).toEqual(["value1", "value2"]); }); @@ -2400,7 +2041,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should timeout and return null with BRPOPLPUSH on empty list", async () => { const redis = ctx.redis; - // Try to move from empty list with short timeout const result = await redis.brpoplpush("empty-source", "dest", 0.1); expect(result).toBeNull(); }); @@ -2409,20 +2049,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "llen-test"; - // Empty list should return 0 const empty = await redis.llen(key); expect(empty).toBe(0); - // Add elements await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); - // Should return correct length const length = await redis.llen(key); expect(length).toBe(3); - // Non-existent key should return 0 const nonExistent = await redis.llen("nonexistent-list"); expect(nonExistent).toBe(0); }); @@ -2431,32 +2067,25 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lpop-test"; - // Push elements await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); - // Pop from head (left side) const first = await redis.lpop(key); expect(first).toBe("one"); - // Pop again const second = await redis.lpop(key); expect(second).toBe("two"); - // Verify remaining element const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual(["three"]); - // Pop last element const last = await redis.lpop(key); expect(last).toBe("three"); - // Pop from empty list should return null const empty = await redis.lpop(key); expect(empty).toBeNull(); - // Non-existent key should return null const nonExistent = await redis.lpop("nonexistent-list"); expect(nonExistent).toBeNull(); }); @@ -2465,32 +2094,25 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "rpop-test"; - // Push elements await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); - // Pop from tail (right side) const first = await redis.rpop(key); expect(first).toBe("three"); - // Pop again const second = await redis.rpop(key); expect(second).toBe("two"); - // Verify remaining element const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual(["one"]); - // Pop last element const last = await redis.rpop(key); expect(last).toBe("one"); - // Pop from empty list should return null const empty = await redis.rpop(key); expect(empty).toBeNull(); - // Non-existent key should return null const nonExistent = await redis.rpop("nonexistent-list"); expect(nonExistent).toBeNull(); }); @@ -2499,26 +2121,20 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "lpushx-test"; - // LPUSHX on non-existent key should return 0 and not create the key const nonExistent = await redis.lpushx(key, "value"); expect(nonExistent).toBe(0); - // Verify key was not created const exists = await redis.exists(key); expect(exists).toBe(false); - // Create the list first await redis.lpush(key, "initial"); - // LPUSHX should now work const result = await redis.lpushx(key, "new"); - expect(result).toBe(2); // New length + expect(result).toBe(2); - // Verify the list const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["new", "initial"]); - // Push multiple elements const result2 = await redis.lpushx(key, "newer"); expect(result2).toBe(3); @@ -2530,26 +2146,20 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "rpushx-test"; - // RPUSHX on non-existent key should return 0 and not create the key const nonExistent = await redis.rpushx(key, "value"); expect(nonExistent).toBe(0); - // Verify key was not created const exists = await redis.exists(key); expect(exists).toBe(false); - // Create the list first await redis.lpush(key, "initial"); - // RPUSHX should now work const result = await redis.rpushx(key, "new"); - expect(result).toBe(2); // New length + expect(result).toBe(2); - // Verify the list const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["initial", "new"]); - // Push multiple elements const result2 = await redis.rpushx(key, "newer"); expect(result2).toBe(3); @@ -2563,16 +2173,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "scard-test"; - // Empty set const count1 = await redis.scard(key); expect(count1).toBe(0); - // Add members await redis.sadd(key, "one", "two", "three"); const count2 = await redis.scard(key); expect(count2).toBe(3); - // Adding duplicate doesn't change count await redis.sadd(key, "two"); const count3 = await redis.scard(key); expect(count3).toBe(3); @@ -2589,7 +2196,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const diff = await redis.sdiff(key1, key2); expect(diff.sort()).toEqual(["a", "b"]); - // Non-existent key const diff2 = await redis.sdiff(key1, "nonexistent"); expect(diff2.sort()).toEqual(["a", "b", "c", "d"]); }); @@ -2618,18 +2224,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(source, "one", "two", "three"); await redis.sadd(dest, "four"); - // Move existing member const result1 = await redis.smove(source, dest, "one"); expect(result1).toBe(true); - // Verify move const sourceMembers = await redis.smembers(source); expect(sourceMembers.sort()).toEqual(["three", "two"]); const destMembers = await redis.smembers(dest); expect(destMembers.sort()).toEqual(["four", "one"]); - // Move non-existent member const result2 = await redis.smove(source, dest, "nonexistent"); expect(result2).toBe(false); }); @@ -2640,25 +2243,20 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key, "one", "two", "three", "four"); - // Pop one member const popped1 = await redis.spop(key); expect(["one", "two", "three", "four"]).toContain(popped1); - // Verify it was removed const remaining1 = await redis.scard(key); expect(remaining1).toBe(3); - // Pop multiple members const popped2 = await redis.spop(key, 2); expect(Array.isArray(popped2)).toBe(true); expect(popped2).toBeDefined(); expect(popped2!.length).toBe(2); - // Verify removed const remaining2 = await redis.scard(key); expect(remaining2).toBe(1); - // Pop from empty set await redis.spop(key); const empty = await redis.spop(key); expect(empty).toBeNull(); @@ -2667,7 +2265,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should publish to sharded channel with SPUBLISH", async () => { const redis = ctx.redis; - // SPUBLISH returns number of subscribers (0 if no one listening) const result = await redis.spublish("test-channel", "test-message"); expect(typeof result).toBe("number"); expect(result).toBeGreaterThanOrEqual(0); @@ -2679,20 +2276,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key, "one", "two", "three"); - // Get one random member const member1 = await redis.srandmember(key); expect(["one", "two", "three"]).toContain(member1); - // Get multiple random members const members = await redis.srandmember(key, 2); expect(Array.isArray(members)).toBe(true); expect(members!.length).toBeLessThanOrEqual(2); - // Verify NOT removed (unlike SPOP) const count = await redis.scard(key); expect(count).toBe(3); - // Empty set const empty = await redis.srandmember("nonexistent"); expect(empty).toBeNull(); }); @@ -2703,19 +2296,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key, "one", "two", "three", "four"); - // Remove one member const count1 = await redis.srem(key, "one"); expect(count1).toBe(1); - // Remove multiple members const count2 = await redis.srem(key, "two", "three"); expect(count2).toBe(2); - // Verify remaining const remaining = await redis.smembers(key); expect(remaining).toEqual(["four"]); - // Remove non-existent member const count3 = await redis.srem(key, "nonexistent"); expect(count3).toBe(0); }); @@ -2731,7 +2320,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const union = await redis.sunion(key1, key2); expect(union.sort()).toEqual(["a", "b", "c", "d", "e"]); - // With non-existent set const union2 = await redis.sunion(key1, "nonexistent"); expect(union2.sort()).toEqual(["a", "b", "c"]); }); @@ -2745,15 +2333,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key1, "a", "b", "c"); await redis.sadd(key2, "c", "d", "e"); - // Store union const count = await redis.sunionstore(dest, key1, key2); expect(count).toBe(5); - // Verify stored const stored = await redis.smembers(dest); expect(stored.sort()).toEqual(["a", "b", "c", "d", "e"]); - // Overwrite existing destination await redis.sadd(dest, "z"); const count2 = await redis.sunionstore(dest, key1, key2); expect(count2).toBe(5); @@ -2768,7 +2353,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "set1"; const key2 = "set2"; - // Create two sets with some overlapping members await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); @@ -2776,7 +2360,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); - // Get intersection const result = await redis.sinter(key1, key2); expect(result.sort()).toEqual(["b", "c"]); }); @@ -2787,7 +2370,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "set2"; const key3 = "set3"; - // Create three sets with one common member await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); @@ -2798,7 +2380,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key3, "d"); await redis.sadd(key3, "e"); - // Get intersection of all three const result = await redis.sinter(key1, key2, key3); expect(result).toEqual(["c"]); }); @@ -2835,7 +2416,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "set2"; const dest = "dest-set"; - // Create two sets with some overlapping members await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); @@ -2843,11 +2423,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); - // Store intersection const count = await redis.sinterstore(dest, key1, key2); expect(count).toBe(2); - // Verify destination has the intersection const members = await redis.smembers(dest); expect(members.sort()).toEqual(["b", "c"]); }); @@ -2858,21 +2436,17 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "set2"; const dest = "dest-set"; - // Create destination with initial data await redis.sadd(dest, "old"); await redis.sadd(dest, "data"); - // Create two sets await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key2, "b"); await redis.sadd(key2, "c"); - // Store intersection (should overwrite destination) const count = await redis.sinterstore(dest, key1, key2); expect(count).toBe(1); - // Verify destination only has the intersection const members = await redis.smembers(dest); expect(members).toEqual(["b"]); }); @@ -2889,7 +2463,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const count = await redis.sinterstore(dest, key1, key2); expect(count).toBe(0); - // Destination should be empty const members = await redis.smembers(dest); expect(members).toEqual([]); }); @@ -2906,7 +2479,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); - // SINTERCARD requires numkeys as first argument const count = await redis.sintercard(2, key1, key2); expect(count).toBe(2); }); @@ -2937,7 +2509,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); - // LIMIT stops counting after reaching the specified number const count = await redis.sintercard(2, key1, key2, "LIMIT", 2); expect(count).toBe(2); }); @@ -2946,7 +2517,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { - // @ts-expect-error no args await redis.sinter(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sinter' command"`); }); @@ -2955,7 +2525,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { - // @ts-expect-error no args await redis.sinterstore(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sinterstore' command"`); }); @@ -2964,7 +2533,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { - // @ts-expect-error no args await redis.sintercard(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sintercard' command"`); }); @@ -2976,7 +2544,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key3 = "set3"; const dest = "diff-result"; - // Set up test sets await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); @@ -2987,16 +2554,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key3, "d"); - // Store difference between key1 and key2 const count1 = await redis.sdiffstore(dest, key1, key2); - expect(count1).toBe(2); // a, d + expect(count1).toBe(2); const members1 = await redis.smembers(dest); expect(members1.sort()).toEqual(["a", "d"]); - // Store difference between key1, key2, and key3 const count2 = await redis.sdiffstore(dest, key1, key2, key3); - expect(count2).toBe(1); // only a + expect(count2).toBe(1); const members2 = await redis.smembers(dest); expect(members2).toEqual(["a"]); @@ -3014,20 +2579,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "test-set"; - // Add some members await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); - // Check which members exist const result = await redis.smismember(key, "a", "b", "d", "e"); - expect(result).toEqual([1, 1, 0, 0]); // a and b exist, d and e don't + expect(result).toEqual([1, 1, 0, 0]); - // Check single member const result2 = await redis.smismember(key, "c"); expect(result2).toEqual([1]); - // Check on non-existent set const result3 = await redis.smismember("nonexistent", "a", "b"); expect(result3).toEqual([0, 0]); }); @@ -3044,12 +2605,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "scan-set"; - // Add multiple members for (let i = 0; i < 20; i++) { await redis.sadd(key, `member${i}`); } - // Scan the set let cursor = "0"; const allMembers: string[] = []; @@ -3059,11 +2618,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { cursor = nextCursor; } while (cursor !== "0"); - // Should have scanned all 20 members expect(allMembers.length).toBe(20); - expect(new Set(allMembers).size).toBe(20); // All unique + expect(new Set(allMembers).size).toBe(20); - // Verify all members exist for (let i = 0; i < 20; i++) { expect(allMembers).toContain(`member${i}`); } @@ -3073,18 +2630,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "scan-pattern-set"; - // Add members with different patterns await redis.sadd(key, "user:1"); await redis.sadd(key, "user:2"); await redis.sadd(key, "user:3"); await redis.sadd(key, "admin:1"); await redis.sadd(key, "admin:2"); - // Scan with MATCH pattern const [cursor, members] = await redis.sscan(key, "0", "MATCH", "user:*"); - // Should only return user members (or could be empty if not in first batch) - // Due to cursor-based scanning, we might need multiple iterations let allUserMembers: string[] = [...members]; let scanCursor = cursor; @@ -3094,16 +2647,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { scanCursor = nextCursor; } - // Filter to only user members const userMembers = allUserMembers.filter(m => m.startsWith("user:")); - expect(userMembers.length).toBeGreaterThanOrEqual(0); // Could be 0 to 3 depending on scan + expect(userMembers.length).toBeGreaterThanOrEqual(0); }); test("should scan empty set with SSCAN", async () => { const redis = ctx.redis; const key = "empty-scan-set"; - // Scan empty set const [cursor, members] = await redis.sscan(key, "0"); expect(cursor).toBe("0"); expect(members).toEqual([]); @@ -3121,20 +2672,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "scard-test"; - // Empty set should have cardinality 0 const emptyCount = await redis.scard(key); expect(emptyCount).toBe(0); - // Add members await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); - // Should have 3 members const count = await redis.scard(key); expect(count).toBe(3); - // Adding duplicate should not change cardinality await redis.sadd(key, "a"); const sameCount = await redis.scard(key); expect(sameCount).toBe(3); @@ -3146,7 +2693,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "sdiff-test2"; const key3 = "sdiff-test3"; - // Set up test sets await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); @@ -3157,19 +2703,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key3, "d"); - // Difference between key1 and key2 const diff1 = await redis.sdiff(key1, key2); expect(diff1.sort()).toEqual(["a", "d"]); - // Difference with multiple sets const diff2 = await redis.sdiff(key1, key2, key3); expect(diff2.sort()).toEqual(["a"]); - // Difference with non-existent set (should return all from first set) const diff3 = await redis.sdiff(key1, "nonexistent"); expect(diff3.sort()).toEqual(["a", "b", "c", "d"]); - // Empty result const diff4 = await redis.sdiff(key2, key1); expect(diff4).toEqual([]); }); @@ -3178,23 +2720,19 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "sismember-test"; - // Add members await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); - // Check existing members const exists1 = await redis.sismember(key, "a"); expect(exists1).toBe(true); const exists2 = await redis.sismember(key, "b"); expect(exists2).toBe(true); - // Check non-existent member const notExists = await redis.sismember(key, "z"); expect(notExists).toBe(false); - // Check on non-existent set const notExistsSet = await redis.sismember("nonexistent", "a"); expect(notExistsSet).toBe(false); }); @@ -3204,32 +2742,25 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const source = "smove-source"; const dest = "smove-dest"; - // Set up source set await redis.sadd(source, "a"); await redis.sadd(source, "b"); await redis.sadd(source, "c"); - // Set up destination set await redis.sadd(dest, "x"); await redis.sadd(dest, "y"); - // Move member from source to dest const moved = await redis.smove(source, dest, "b"); expect(moved).toBe(true); - // Verify source no longer has the member const sourceMembers = await redis.smembers(source); expect(sourceMembers.sort()).toEqual(["a", "c"]); - // Verify dest has the member const destMembers = await redis.smembers(dest); expect(destMembers.sort()).toEqual(["b", "x", "y"]); - // Try to move non-existent member const notMoved = await redis.smove(source, dest, "z"); expect(notMoved).toBe(false); - // Try to move from non-existent set const notMoved2 = await redis.smove("nonexistent", dest, "a"); expect(notMoved2).toBe(false); }); @@ -3238,23 +2769,19 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "spop-test"; - // Add members await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); await redis.sadd(key, "d"); await redis.sadd(key, "e"); - // Pop single member const popped = await redis.spop(key); expect(popped).toBeDefined(); expect(["a", "b", "c", "d", "e"]).toContain(popped); - // Verify member was removed const remaining = await redis.scard(key); expect(remaining).toBe(4); - // Pop multiple members const poppedMultiple = await redis.spop(key, 2); expect(Array.isArray(poppedMultiple)).toBe(true); expect(poppedMultiple!.length).toBe(2); @@ -3262,12 +2789,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(["a", "b", "c", "d", "e"]).toContain(member); }); - // Verify members were removed const remainingAfter = await redis.scard(key); expect(remainingAfter).toBe(2); - // Pop from empty set - await redis.spop(key, 10); // Pop all remaining + await redis.spop(key, 10); const emptyPop = await redis.spop(key); expect(emptyPop).toBeNull(); }); @@ -3276,8 +2801,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const channel = "spublish-channel"; - // SPUBLISH returns number of subscribers - // With no subscribers, should return 0 const count = await redis.spublish(channel, "test message"); expect(typeof count).toBe("number"); expect(count).toBe(0); @@ -3287,23 +2810,19 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "srandmember-test"; - // Add members await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); await redis.sadd(key, "d"); await redis.sadd(key, "e"); - // Get single random member const random = await redis.srandmember(key); expect(random).toBeDefined(); expect(["a", "b", "c", "d", "e"]).toContain(random); - // Verify member was NOT removed const count = await redis.scard(key); expect(count).toBe(5); - // Get multiple random members const randomMultiple = await redis.srandmember(key, 3); expect(Array.isArray(randomMultiple)).toBe(true); expect(randomMultiple!.length).toBe(3); @@ -3311,23 +2830,19 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(["a", "b", "c", "d", "e"]).toContain(member); }); - // Verify members were NOT removed const countAfter = await redis.scard(key); expect(countAfter).toBe(5); - // Get with count larger than set size (no duplicates) const tooMany = await redis.srandmember(key, 10); expect(Array.isArray(tooMany)).toBe(true); expect(tooMany!.length).toBe(5); - // Get with negative count (allows duplicates) const withDuplicates = await redis.srandmember(key, -10); expect(withDuplicates!.length).toBe(10); withDuplicates!.forEach(member => { expect(["a", "b", "c", "d", "e"]).toContain(member); }); - // Get from empty set const emptyRandom = await redis.srandmember("nonexistent"); expect(emptyRandom).toBeNull(); }); @@ -3336,34 +2851,27 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "srem-test"; - // Add members await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); await redis.sadd(key, "d"); await redis.sadd(key, "e"); - // Remove single member const removed1 = await redis.srem(key, "a"); expect(removed1).toBe(1); - // Verify member was removed const members1 = await redis.smembers(key); expect(members1.sort()).toEqual(["b", "c", "d", "e"]); - // Remove multiple members const removed2 = await redis.srem(key, "b", "c", "z"); - expect(removed2).toBe(2); // "z" doesn't exist, so only 2 removed + expect(removed2).toBe(2); - // Verify members were removed const members2 = await redis.smembers(key); expect(members2.sort()).toEqual(["d", "e"]); - // Remove non-existent member const removed3 = await redis.srem(key, "nonexistent"); expect(removed3).toBe(0); - // Remove from non-existent set const removed4 = await redis.srem("nonexistent", "a"); expect(removed4).toBe(0); }); @@ -3374,7 +2882,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "sunion-test2"; const key3 = "sunion-test3"; - // Set up test sets await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); @@ -3387,19 +2894,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key3, "f"); await redis.sadd(key3, "g"); - // Union of two sets const union1 = await redis.sunion(key1, key2); expect(union1.sort()).toEqual(["a", "b", "c", "d", "e"]); - // Union of three sets const union2 = await redis.sunion(key1, key2, key3); expect(union2.sort()).toEqual(["a", "b", "c", "d", "e", "f", "g"]); - // Union with non-existent set const union3 = await redis.sunion(key1, "nonexistent"); expect(union3.sort()).toEqual(["a", "b", "c"]); - // Union with empty set const union4 = await redis.sunion("nonexistent1", "nonexistent2"); expect(union4).toEqual([]); }); @@ -3411,7 +2914,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key3 = "sunionstore-test3"; const dest = "sunionstore-dest"; - // Set up test sets await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); @@ -3423,35 +2925,27 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.sadd(key3, "e"); await redis.sadd(key3, "f"); - // Store union of two sets const count1 = await redis.sunionstore(dest, key1, key2); expect(count1).toBe(5); - // Verify destination has the union const members1 = await redis.smembers(dest); expect(members1.sort()).toEqual(["a", "b", "c", "d", "e"]); - // Store union of three sets (should overwrite destination) const count2 = await redis.sunionstore(dest, key1, key2, key3); expect(count2).toBe(6); - // Verify destination was overwritten const members2 = await redis.smembers(dest); expect(members2.sort()).toEqual(["a", "b", "c", "d", "e", "f"]); - // Store union with non-existent set const count3 = await redis.sunionstore(dest, key1, "nonexistent"); expect(count3).toBe(3); - // Verify destination has only key1 members const members3 = await redis.smembers(dest); expect(members3.sort()).toEqual(["a", "b", "c"]); - // Store empty union const count4 = await redis.sunionstore(dest, "nonexistent1", "nonexistent2"); expect(count4).toBe(0); - // Verify destination is empty const members4 = await redis.smembers(dest); expect(members4).toEqual([]); }); @@ -3462,18 +2956,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zcard-test"; - // Empty set should return 0 const count1 = await redis.zcard(key); expect(count1).toBe(0); - // Add members await redis.zadd(key, 1, "one", 2, "two", 3, "three"); - // Get cardinality const count2 = await redis.zcard(key); expect(count2).toBe(3); - // Add duplicate (updates score, doesn't increase count) await redis.zadd(key, 4, "one"); const count3 = await redis.zcard(key); expect(count3).toBe(3); @@ -3483,25 +2973,20 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zpopmax-test"; - // Add members with scores await redis.zadd(key, 1, "one", 2, "two", 3, "three", 4, "four"); - // Pop one max member const result1 = await redis.zpopmax(key); expect(result1).toEqual(["four", 4]); - // Pop two max members const result2 = await redis.zpopmax(key, 2); expect(result2).toEqual([ ["three", 3], ["two", 2], ]); - // Verify remaining member const remaining = await redis.zcard(key); expect(remaining).toBe(1); - // Pop from empty set await redis.zpopmax(key); const empty = await redis.zpopmax(key); expect(empty).toEqual([]); @@ -3511,25 +2996,20 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zpopmin-test"; - // Add members with scores await redis.zadd(key, 1, "one", 2, "two", 3, "three", 4, "four"); - // Pop one min member const result1 = await redis.zpopmin(key); expect(result1).toEqual(["one", 1]); - // Pop two min members const result2 = await redis.zpopmin(key, 2); expect(result2).toEqual([ ["two", 2], ["three", 3], ]); - // Verify remaining member const remaining = await redis.zcard(key); expect(remaining).toBe(1); - // Pop from empty set await redis.zpopmin(key); const empty = await redis.zpopmin(key); expect(empty).toEqual([]); @@ -3539,15 +3019,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrandmember-test"; - // Add members await redis.zadd(key, 1, "one", 2, "two", 3, "three"); - // Get one random member const result1 = await redis.zrandmember(key); expect(result1).toBeDefined(); expect(["one", "two", "three"]).toContain(result1); - // Get multiple random members (without scores) const result2 = await redis.zrandmember(key, 2); expect(Array.isArray(result2)).toBe(true); expect(result2!.length).toBeLessThanOrEqual(2); @@ -3556,12 +3033,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(["one", "two", "three"]).toContain(member); }); - // Get with scores const result3 = await redis.zrandmember(key, 1, "WITHSCORES"); expect<([string, number][] | null)[]>([[["one", 1]], [["two", 2]], [["three", 3]]]).toContainEqual(result3); - // Empty set const emptyKey = "zrandmember-empty-" + randomUUIDv7(); const empty = await redis.zrandmember(emptyKey); expect(empty).toBeNull(); @@ -3571,10 +3046,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrank-test"; - // Add members with scores await redis.zadd(key, 1, "one", 2, "two", 3, "three"); - // Get ranks (0-indexed, ascending) const rank1 = await redis.zrank(key, "one"); expect(rank1).toBe(0); @@ -3584,7 +3057,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const rank3 = await redis.zrank(key, "three"); expect(rank3).toBe(2); - // Non-existent member const rank4 = await redis.zrank(key, "nonexistent"); expect(rank4).toBeNull(); }); @@ -3593,10 +3065,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrank-test"; - // Add members with scores await redis.zadd(key, 1, "one", 2, "two", 3, "three"); - // Get reverse ranks (0-indexed, descending) const rank1 = await redis.zrevrank(key, "three"); expect(rank1).toBe(0); @@ -3606,7 +3076,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const rank3 = await redis.zrevrank(key, "one"); expect(rank3).toBe(2); - // Non-existent member const rank4 = await redis.zrevrank(key, "nonexistent"); expect(rank4).toBeNull(); }); @@ -3615,18 +3084,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zincrby-test"; - // Add initial members await redis.send("ZADD", [key, "1.0", "member1", "2.0", "member2"]); - // Increment member1's score by 2.5 const newScore1 = await redis.zincrby(key, 2.5, "member1"); expect(newScore1).toBe(3.5); - // Increment member2's score by -1.5 const newScore2 = await redis.zincrby(key, -1.5, "member2"); expect(newScore2).toBe(0.5); - // Increment non-existent member (should create it with the increment as score) const newScore3 = await redis.zincrby(key, 5, "member3"); expect(newScore3).toBe(5); }); @@ -3635,22 +3100,17 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zcount-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Count all members const count1 = await redis.zcount(key, "-inf", "+inf"); expect(count1).toBe(5); - // Count members with score between 2 and 4 (inclusive) const count2 = await redis.zcount(key, 2, 4); - expect(count2).toBe(3); // two, three, four + expect(count2).toBe(3); - // Count with specific range const count3 = await redis.zcount(key, 1, 3); - expect(count3).toBe(3); // one, two, three + expect(count3).toBe(3); - // Count with no matches const count4 = await redis.zcount(key, 10, 20); expect(count4).toBe(0); }); @@ -3659,22 +3119,17 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zlexcount-test"; - // Add members with same score (required for lex operations) await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); - // Count all members const count1 = await redis.zlexcount(key, "-", "+"); expect(count1).toBe(5); - // Count members from "banana" to "date" (inclusive) const count2 = await redis.zlexcount(key, "[banana", "[date"); - expect(count2).toBe(3); // banana, cherry, date + expect(count2).toBe(3); - // Count with exclusive range const count3 = await redis.zlexcount(key, "(banana", "(date"); - expect(count3).toBe(1); // only cherry + expect(count3).toBe(1); - // Count with no matches const count4 = await redis.zlexcount(key, "[zebra", "[zoo"); expect(count4).toBe(0); }); @@ -3685,32 +3140,26 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zdiff-test2"; const key3 = "zdiff-test3"; - // Set up sorted sets await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three", "4", "four"]); await redis.send("ZADD", [key2, "1", "one", "2", "two"]); await redis.send("ZADD", [key3, "3", "three"]); - // Difference between first and second set const diff1 = await redis.zdiff(2, key1, key2); expect(diff1).toEqual(["three", "four"]); - // Difference with multiple sets const diff2 = await redis.zdiff(3, key1, key2, key3); expect(diff2).toEqual(["four"]); - // Difference with WITHSCORES const diff3 = await redis.zdiff(2, key1, key2, "WITHSCORES"); expect(diff3).toEqual([ ["three", 3], ["four", 4], ]); - // Difference with non-existent set (should return all from first set) const diff4 = await redis.zdiff(2, key1, "nonexistent"); expect(diff4.length).toBe(4); expect(diff4).toEqual(["one", "two", "three", "four"]); - // Empty result const diff5 = await redis.zdiff(2, key2, key1); expect(diff5).toEqual([]); }); @@ -3721,30 +3170,24 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zdiffstore-test2"; const dest = "zdiffstore-dest"; - // Set up sorted sets await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three"]); await redis.send("ZADD", [key2, "1", "one"]); - // Store the difference const count = await redis.zdiffstore(dest, 2, key1, key2); expect(count).toBe(2); - // Verify the destination has the correct members const members = await redis.send("ZRANGE", [dest, "0", "-1"]); expect(members).toEqual(["two", "three"]); - // Verify scores are preserved const membersWithScores = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(membersWithScores).toEqual([ ["two", 2], ["three", 3], ]); - // Store empty result (should overwrite existing key) const count2 = await redis.zdiffstore(dest, 2, key2, key1); expect(count2).toBe(0); - // Verify destination is now empty const finalCount = await redis.send("ZCARD", [dest]); expect(finalCount).toBe(0); }); @@ -3755,28 +3198,22 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zintercard-test2"; const key3 = "zintercard-test3"; - // Set up sorted sets await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three"]); await redis.send("ZADD", [key2, "1", "one", "2", "two", "4", "four"]); await redis.send("ZADD", [key3, "1", "one", "5", "five"]); - // Basic intersection count const count1 = await redis.zintercard(2, key1, key2); - expect(count1).toBe(2); // one and two + expect(count1).toBe(2); - // Intersection of three sets const count2 = await redis.zintercard(3, key1, key2, key3); - expect(count2).toBe(1); // only one + expect(count2).toBe(1); - // With LIMIT const count3 = await redis.zintercard(2, key1, key2, "LIMIT", 1); - expect(count3).toBe(1); // stopped at limit + expect(count3).toBe(1); - // No intersection const count4 = await redis.zintercard(2, key1, key3); - expect(count4).toBe(1); // only one exists in both + expect(count4).toBe(1); - // With non-existent set const count5 = await redis.zintercard(2, key1, "nonexistent"); expect(count5).toBe(0); }); @@ -3810,22 +3247,17 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zremrangebyrank-test"; - // Add members await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Remove first 2 members (rank 0 and 1) const removed1 = await redis.zremrangebyrank(key, 0, 1); expect(removed1).toBe(2); - // Verify remaining members const remaining = await redis.send("ZCARD", [key]); expect(remaining).toBe(3); - // Remove last member (negative index) const removed2 = await redis.zremrangebyrank(key, -1, -1); expect(removed2).toBe(1); - // Verify 2 members remain const final = await redis.send("ZCARD", [key]); expect(final).toBe(2); }); @@ -3834,18 +3266,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zremrangebyscore-test"; - // Add members await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Remove members with score 2-4 (inclusive) const removed1 = await redis.zremrangebyscore(key, 2, 4); - expect(removed1).toBe(3); // two, three, four + expect(removed1).toBe(3); - // Verify remaining members const remaining = await redis.send("ZCARD", [key]); - expect(remaining).toBe(2); // one and five + expect(remaining).toBe(2); - // Remove with infinity const removed2 = await redis.zremrangebyscore(key, "-inf", "+inf"); expect(removed2).toBe(2); }); @@ -3854,18 +3282,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zremrangebylex-test"; - // Add members with same score await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); - // Remove from "banana" to "date" (inclusive) const removed1 = await redis.zremrangebylex(key, "[banana", "[date"); - expect(removed1).toBe(3); // banana, cherry, date + expect(removed1).toBe(3); - // Verify remaining members const remaining = await redis.send("ZCARD", [key]); - expect(remaining).toBe(2); // apple and elderberry + expect(remaining).toBe(2); - // Remove remaining with open range const removed2 = await redis.zremrangebylex(key, "-", "+"); expect(removed2).toBe(2); }); @@ -3916,46 +3340,36 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrem-test"; - // Add members await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); - // Remove single member const removed1 = await redis.zrem(key, "two"); expect(removed1).toBe(1); - // Remove multiple members const removed2 = await redis.zrem(key, "one", "three"); expect(removed2).toBe(2); - // Remove non-existent member const removed3 = await redis.zrem(key, "nonexistent"); expect(removed3).toBe(0); - // Remove mix of existing and non-existing const removed4 = await redis.zrem(key, "four", "nothere"); - expect(removed4).toBe(1); // Only "four" was removed + expect(removed4).toBe(1); }); test("should get scores with ZMSCORE", async () => { const redis = ctx.redis; const key = "zmscore-test"; - // Add members with scores await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); - // Get single score const scores1 = await redis.zmscore(key, "two"); expect(scores1).toEqual([2.7]); - // Get multiple scores const scores2 = await redis.zmscore(key, "one", "three"); expect(scores2).toEqual([1.5, 3.9]); - // Get mix of existing and non-existing members const scores3 = await redis.zmscore(key, "one", "nonexistent", "three"); expect(scores3).toEqual([1.5, null, 3.9]); - // Get all non-existent members const scores4 = await redis.zmscore(key, "nothere", "alsonothere"); expect(scores4).toEqual([null, null]); }); @@ -3980,19 +3394,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zadd-basic-test"; - // Add single member const added1 = await redis.zadd(key, "1", "one"); expect(added1).toBe(1); - // Add multiple members const added2 = await redis.zadd(key, "2", "two", "3", "three"); expect(added2).toBe(2); - // Update existing member (should return 0 since no new members added) const added3 = await redis.zadd(key, "1.5", "one"); expect(added3).toBe(0); - // Verify members were added/updated const score = await redis.zscore(key, "one"); expect(score).toBe(1.5); }); @@ -4001,18 +3411,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zadd-nx-test"; - // Add initial member await redis.zadd(key, "1", "one"); - // Try to add with NX (should fail since member exists) const added1 = await redis.zadd(key, "NX", "2", "one"); expect(added1).toBe(0); - // Verify score wasn't updated const score1 = await redis.zscore(key, "one"); expect(score1).toBe(1); - // Add new member with NX (should succeed) const added2 = await redis.zadd(key, "NX", "2", "two"); expect(added2).toBe(1); @@ -4024,22 +3430,17 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zadd-xx-test"; - // Add initial member await redis.zadd(key, "1", "one"); - // Update with XX (should succeed) const updated1 = await redis.zadd(key, "XX", "2", "one"); - expect(updated1).toBe(0); // No new members added + expect(updated1).toBe(0); - // Verify score was updated const score1 = await redis.zscore(key, "one"); expect(score1).toBe(2); - // Try to add new member with XX (should fail) const added = await redis.zadd(key, "XX", "3", "three"); expect(added).toBe(0); - // Verify member wasn't added const score2 = await redis.zscore(key, "three"); expect(score2).toBeNull(); }); @@ -4048,26 +3449,21 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zadd-ch-test"; - // Add initial members await redis.zadd(key, "1", "one", "2", "two"); - // Add and update with CH (should return total changed count) const changed = await redis.zadd(key, "CH", "1.5", "one", "3", "three"); - expect(changed).toBe(2); // one updated, three added + expect(changed).toBe(2); }); test("should increment score with INCR option in ZADD", async () => { const redis = ctx.redis; const key = "zadd-incr-test"; - // Add initial member await redis.zadd(key, "1", "one"); - // Increment with INCR option (returns new score as string) const newScore = await redis.zadd(key, "INCR", "2.5", "one"); expect(newScore).toBe(3.5); - // Verify the score const score = await redis.zscore(key, "one"); expect(score).toBe(3.5); }); @@ -4076,22 +3472,17 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zadd-gt-test"; - // Add initial member await redis.zadd(key, "5", "one"); - // Try to update with lower score and GT (should fail) const updated1 = await redis.zadd(key, "GT", "3", "one"); expect(updated1).toBe(0); - // Verify score wasn't updated const score1 = await redis.zscore(key, "one"); expect(score1).toBe(5); - // Update with higher score and GT (should succeed) const updated2 = await redis.zadd(key, "GT", "7", "one"); - expect(updated2).toBe(0); // No new members added + expect(updated2).toBe(0); - // Verify score was updated const score2 = await redis.zscore(key, "one"); expect(score2).toBe(7); }); @@ -4100,22 +3491,17 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zadd-lt-test"; - // Add initial member await redis.zadd(key, "5", "one"); - // Try to update with higher score and LT (should fail) const updated1 = await redis.zadd(key, "LT", "7", "one"); expect(updated1).toBe(0); - // Verify score wasn't updated const score1 = await redis.zscore(key, "one"); expect(score1).toBe(5); - // Update with lower score and LT (should succeed) const updated2 = await redis.zadd(key, "LT", "3", "one"); - expect(updated2).toBe(0); // No new members added + expect(updated2).toBe(0); - // Verify score was updated const score2 = await redis.zscore(key, "one"); expect(score2).toBe(3); }); @@ -4124,10 +3510,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zscan-test"; - // Add members await redis.zadd(key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"); - // Scan all elements let cursor = "0"; const allElements: string[] = []; do { @@ -4136,10 +3520,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { cursor = nextCursor; } while (cursor !== "0"); - // Should have member-score pairs (10 elements total: 5 members + 5 scores) expect(allElements.length).toBe(10); - // Verify we got all members (check every other element for member names) const members = allElements.filter((_, index) => index % 2 === 0); expect(members).toContain("one"); expect(members).toContain("two"); @@ -4152,10 +3534,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zscan-match-test"; - // Add members with different patterns await redis.zadd(key, "1", "user:1", "2", "user:2", "3", "post:1", "4", "post:2"); - // Scan with MATCH pattern let cursor = "0"; const userElements: string[] = []; do { @@ -4164,10 +3544,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { cursor = nextCursor; } while (cursor !== "0"); - // Extract member names (every other element) const members = userElements.filter((_, index) => index % 2 === 0); - // Should only find user keys expect(members).toContain("user:1"); expect(members).toContain("user:2"); expect(members).not.toContain("post:1"); @@ -4178,14 +3556,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zscan-count-test"; - // Add many members const promises: Promise[] = []; for (let i = 0; i < 100; i++) { promises.push(redis.zadd(key, String(i), `member:${i}`)); } await Promise.all(promises); - // Scan with COUNT hint - loop until cursor returns to "0" let cursor = "0"; const allElements: string[] = []; do { @@ -4194,17 +3570,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { cursor = nextCursor; } while (cursor !== "0"); - // Should have member-score pairs (200 elements total: 100 members + 100 scores) expect(allElements.length).toBe(200); - // Verify we got all members (check every other element for member names) const members = allElements.filter((_, index) => index % 2 === 0); expect(members.length).toBe(100); for (let i = 0; i < 100; i++) { expect(members).toContain(`member:${i}`); } - // Test with numeric cursor cursor = 0 as any; const allElements2: string[] = []; do { @@ -4234,18 +3607,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrange-basic-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get all members const all = await redis.zrange(key, 0, -1); expect(all).toEqual(["one", "two", "three", "four", "five"]); - // Get first 3 members const first3 = await redis.zrange(key, 0, 2); expect(first3).toEqual(["one", "two", "three"]); - // Get last 2 members using negative indices const last2 = await redis.zrange(key, -2, -1); expect(last2).toEqual(["four", "five"]); }); @@ -4254,10 +3623,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrange-withscores-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2.5", "two", "3", "three"]); - // Get members with scores const result = await redis.zrange(key, 0, -1, "WITHSCORES"); expect(result).toEqual([ ["one", 1], @@ -4270,18 +3637,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrange-byscore-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get members with score 2-4 (inclusive) const range1 = await redis.zrange(key, "2", "4", "BYSCORE"); expect(range1).toEqual(["two", "three", "four"]); - // Get members with score > 2 and <= 4 (exclusive start) const range2 = await redis.zrange(key, "(2", "4", "BYSCORE"); expect(range2).toEqual(["three", "four"]); - // Get all members using infinity const all = await redis.zrange(key, "-inf", "+inf", "BYSCORE"); expect(all).toEqual(["one", "two", "three", "four", "five"]); }); @@ -4290,14 +3653,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrange-rev-test"; - // Add members await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); - // Get members in reverse order const reversed = await redis.zrange(key, 0, -1, "REV"); expect(reversed).toEqual(["three", "two", "one"]); - // Get top 2 with highest scores const top2 = await redis.zrange(key, 0, 1, "REV"); expect(top2).toEqual(["three", "two"]); }); @@ -4306,10 +3666,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrange-limit-test"; - // Add members await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get members with score >= 1, limit 2 starting from offset 1 const result = await redis.zrange(key, "1", "5", "BYSCORE", "LIMIT", "1", "2"); expect(result).toEqual(["two", "three"]); }); @@ -4318,14 +3676,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrange-bylex-test"; - // Add members with same score (required for lex operations) await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date"]); - // Get range from "banana" to "cherry" (inclusive) const range1 = await redis.zrange(key, "[banana", "[cherry", "BYLEX"); expect(range1).toEqual(["banana", "cherry"]); - // Get range with exclusive bounds const range2 = await redis.zrange(key, "(banana", "(date", "BYLEX"); expect(range2).toEqual(["cherry"]); }); @@ -4334,18 +3689,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrange-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get all members in reverse order const all = await redis.zrevrange(key, 0, -1); expect(all).toEqual(["five", "four", "three", "two", "one"]); - // Get top 3 members with highest scores const top3 = await redis.zrevrange(key, 0, 2); expect(top3).toEqual(["five", "four", "three"]); - // Get members using negative indices const last2 = await redis.zrevrange(key, -2, -1); expect(last2).toEqual(["two", "one"]); }); @@ -4354,10 +3705,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrange-withscores-test"; - // Add members with scores await redis.send("ZADD", [key, "1.5", "one", "2", "two", "3.7", "three"]); - // Get members with scores in reverse order const result = await redis.zrevrange(key, 0, -1, "WITHSCORES"); expect(result).toEqual([ ["three", 3.7], @@ -4370,7 +3719,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrange-empty-test"; - // Query empty set const result = await redis.zrange(key, 0, -1); expect(result).toEqual([]); }); @@ -4379,7 +3727,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrange-empty-test"; - // Query empty set const result = await redis.zrevrange(key, 0, -1); expect(result).toEqual([]); }); @@ -4403,26 +3750,20 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrangebyscore-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get all members const all = await redis.zrangebyscore(key, "-inf", "+inf"); expect(all).toEqual(["one", "two", "three", "four", "five"]); - // Get members with score 2-4 (inclusive) const range1 = await redis.zrangebyscore(key, 2, 4); expect(range1).toEqual(["two", "three", "four"]); - // Get members with exclusive lower bound const range2 = await redis.zrangebyscore(key, "(2", 4); expect(range2).toEqual(["three", "four"]); - // Get members with exclusive upper bound const range3 = await redis.zrangebyscore(key, 2, "(4"); expect(range3).toEqual(["two", "three"]); - // Get members with both exclusive bounds const range4 = await redis.zrangebyscore(key, "(2", "(4"); expect(range4).toEqual(["three"]); }); @@ -4431,10 +3772,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrangebyscore-withscores-test"; - // Add members await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); - // Get with scores const result = await redis.zrangebyscore(key, 1, 3, "WITHSCORES"); expect(result).toEqual([ ["one", 1.5], @@ -4446,18 +3785,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrangebyscore-limit-test"; - // Add members await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get first 2 members in score range const limited1 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 0, 2); expect(limited1).toEqual(["one", "two"]); - // Skip first, get next 2 const limited2 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 1, 2); expect(limited2).toEqual(["two", "three"]); - // Can combine with score range const limited3 = await redis.zrangebyscore(key, 2, 5, "LIMIT", 1, 2); expect(limited3).toEqual(["three", "four"]); }); @@ -4481,10 +3816,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrangebyscore-combined-test"; - // Add members await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); - // Get with both options const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES", "LIMIT", 1, 2); expect(result).toEqual([ ["two", 2], @@ -4496,22 +3829,17 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrangebyscore-test"; - // Add members with scores (note: max comes before min in ZREVRANGEBYSCORE) await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get all members in reverse order const all = await redis.zrevrangebyscore(key, "+inf", "-inf"); expect(all).toEqual(["five", "four", "three", "two", "one"]); - // Get members with score 4-2 (note: max=4, min=2) const range1 = await redis.zrevrangebyscore(key, 4, 2); expect(range1).toEqual(["four", "three", "two"]); - // Get with exclusive bounds const range2 = await redis.zrevrangebyscore(key, "(4", "(2"); expect(range2).toEqual(["three"]); - // Get with one exclusive bound const range3 = await redis.zrevrangebyscore(key, 4, "(2"); expect(range3).toEqual(["four", "three"]); }); @@ -4520,10 +3848,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrangebyscore-withscores-test"; - // Add members await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); - // Get with scores (max=3, min=1) const result = await redis.zrevrangebyscore(key, 3, 1, "WITHSCORES"); expect(result).toEqual([ ["two", 2.7], @@ -4535,14 +3861,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrangebyscore-limit-test"; - // Add members await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get first 2 members in reverse const limited1 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 0, 2); expect(limited1).toEqual(["five", "four"]); - // Skip first, get next 2 const limited2 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 1, 2); expect(limited2).toEqual(["four", "three"]); }); @@ -4551,30 +3874,23 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrangebylex-test"; - // Add members with same score (required for lex operations) await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); - // Get all members const all = await redis.zrangebylex(key, "-", "+"); expect(all).toEqual(["apple", "banana", "cherry", "date", "elderberry"]); - // Get range from "banana" to "date" (inclusive) const range1 = await redis.zrangebylex(key, "[banana", "[date"); expect(range1).toEqual(["banana", "cherry", "date"]); - // Get range with exclusive bounds const range2 = await redis.zrangebylex(key, "(banana", "(date"); expect(range2).toEqual(["cherry"]); - // Get range with one exclusive, one inclusive const range3 = await redis.zrangebylex(key, "[banana", "(date"); expect(range3).toEqual(["banana", "cherry"]); - // Get range from start to specific member const range4 = await redis.zrangebylex(key, "-", "[cherry"); expect(range4).toEqual(["apple", "banana", "cherry"]); - // Get range from specific member to end const range5 = await redis.zrangebylex(key, "[cherry", "+"); expect(range5).toEqual(["cherry", "date", "elderberry"]); }); @@ -4583,18 +3899,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrangebylex-limit-test"; - // Add members with same score await redis.send("ZADD", [key, "0", "a", "0", "b", "0", "c", "0", "d", "0", "e", "0", "f", "0", "g"]); - // Get first 3 members const limited1 = await redis.zrangebylex(key, "-", "+", "LIMIT", 0, 3); expect(limited1).toEqual(["a", "b", "c"]); - // Skip first 2, get next 3 const limited2 = await redis.zrangebylex(key, "-", "+", "LIMIT", 2, 3); expect(limited2).toEqual(["c", "d", "e"]); - // Get last 2 with large offset const limited3 = await redis.zrangebylex(key, "-", "+", "LIMIT", 5, 10); expect(limited3).toEqual(["f", "g"]); }); @@ -4603,21 +3915,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zcard-test"; - // Check cardinality of non-existent key const count0 = await redis.zcard(key); expect(count0).toBe(0); - // Add members to sorted set await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); - // Check cardinality const count1 = await redis.zcard(key); expect(count1).toBe(3); - // Add more members await redis.send("ZADD", [key, "4", "four", "5", "five"]); - // Check updated cardinality const count2 = await redis.zcard(key); expect(count2).toBe(5); }); @@ -4642,10 +3949,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zpopmin-count-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Pop 3 members with lowest scores const result = await redis.zpopmin(key, 3); expect(result).toEqual([ ["one", 1], @@ -4653,7 +3958,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { ["three", 3], ]); - // Verify remaining count const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); @@ -4662,7 +3966,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const emptyKey = "zpopmin-empty-test"; - // Try to pop from empty set const result = await redis.zpopmin(emptyKey); expect(result).toEqual([]); }); @@ -4671,17 +3974,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zpopmin-tie-test"; - // Add members with same scores await redis.send("ZADD", [key, "1", "a", "1", "b", "1", "c", "2", "d"]); - // Pop 2 members (should get lexicographically first members with same score) const result = await redis.zpopmin(key, 2); expect(result).toEqual([ ["a", 1], ["b", 1], ]); - // Verify remaining count const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); @@ -4706,10 +4006,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zpopmax-count-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Pop 3 members with highest scores const result = await redis.zpopmax(key, 3); expect(result).toEqual([ ["five", 5], @@ -4717,7 +4015,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { ["three", 3], ]); - // Verify remaining count const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); @@ -4726,7 +4023,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const emptyKey = "zpopmax-empty-test"; - // Try to pop from empty set const result = await redis.zpopmax(emptyKey); expect(result).toEqual([]); }); @@ -4735,17 +4031,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zpopmax-tie-test"; - // Add members with same scores await redis.send("ZADD", [key, "1", "a", "2", "b", "2", "c", "2", "d"]); - // Pop 2 members (should get lexicographically last members with same score) const result = await redis.zpopmax(key, 2); expect(result).toEqual([ ["d", 2], ["c", 2], ]); - // Verify remaining count const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); @@ -4754,10 +4047,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrandmember-test"; - // Add members await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get single random member const result = await redis.zrandmember(key); expect(result).toBeDefined(); expect(typeof result).toBe("string"); @@ -4768,7 +4059,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const emptyKey = "zrandmember-empty-test"; - // Try to get random member from empty set const result = await redis.zrandmember(emptyKey); expect(result).toBeNull(); }); @@ -4796,10 +4086,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrandmember-withscores-test"; - // Add members await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); - // Get 2 random members with scores const result = await redis.zrandmember(key, 2, "WITHSCORES"); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); @@ -4818,10 +4106,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrandmember-negative-test"; - // Add members await redis.send("ZADD", [key, "1", "one", "2", "two"]); - // Get 5 random members with negative count (allows duplicates) const result = await redis.zrandmember(key, -5); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); @@ -4836,10 +4122,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrank-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get rank of members (0-indexed, lowest score has rank 0) const rank1 = await redis.zrank(key, "one"); expect(rank1).toBe(0); @@ -4849,7 +4133,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const rank3 = await redis.zrank(key, "five"); expect(rank3).toBe(4); - // Non-existent member should return null const rank4 = await redis.zrank(key, "nonexistent"); expect(rank4).toBeNull(); }); @@ -4858,18 +4141,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrank-withscore-test"; - // Add members with scores await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); - // Get rank with score const result = await redis.zrank(key, "two", "WITHSCORE"); expect(result).toEqual([1, 2.7]); - // First member const result2 = await redis.zrank(key, "one", "WITHSCORE"); expect(result2).toEqual([0, 1.5]); - // Non-existent member should return null const result3 = await redis.zrank(key, "nonexistent", "WITHSCORE"); expect(result3).toBeNull(); }); @@ -4878,10 +4157,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrank-tie-test"; - // Add members with same scores await redis.send("ZADD", [key, "1", "a", "1", "b", "1", "c", "2", "d"]); - // Members with same score are ordered lexicographically const rankA = await redis.zrank(key, "a"); expect(rankA).toBe(0); @@ -4899,10 +4176,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrank-test"; - // Add members with scores await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Get reverse rank (0-indexed from highest score) const rank1 = await redis.zrevrank(key, "five"); expect(rank1).toBe(0); @@ -4912,7 +4187,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const rank3 = await redis.zrevrank(key, "one"); expect(rank3).toBe(4); - // Non-existent member should return null const rank4 = await redis.zrevrank(key, "nonexistent"); expect(rank4).toBeNull(); }); @@ -4921,18 +4195,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrank-withscore-test"; - // Add members with scores await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); - // Get reverse rank with score const result = await redis.zrevrank(key, "two", "WITHSCORE"); expect(result).toEqual([1, 2.7]); - // Highest score member (reverse rank 0) const result2 = await redis.zrevrank(key, "three", "WITHSCORE"); expect(result2).toEqual([0, 3.9]); - // Non-existent member should return null const result3 = await redis.zrevrank(key, "nonexistent", "WITHSCORE"); expect(result3).toBeNull(); }); @@ -4941,10 +4211,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrank-tie-test"; - // Add members with same scores await redis.send("ZADD", [key, "1", "a", "2", "b", "2", "c", "2", "d"]); - // Members with same score are ordered lexicographically (reverse) const rankD = await redis.zrevrank(key, "d"); expect(rankD).toBe(0); @@ -4986,22 +4254,17 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrangebylex-test"; - // Add members with same score (required for lex operations) await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); - // Get all members in reverse order const all = await redis.zrevrangebylex(key, "+", "-"); expect(all).toEqual(["elderberry", "date", "cherry", "banana", "apple"]); - // Get range from "date" to "banana" (inclusive, reverse order) const range1 = await redis.zrevrangebylex(key, "[date", "[banana"); expect(range1).toEqual(["date", "cherry", "banana"]); - // Get range with exclusive bounds const range2 = await redis.zrevrangebylex(key, "(date", "(banana"); expect(range2).toEqual(["cherry"]); - // Get range with one exclusive, one inclusive const range3 = await redis.zrevrangebylex(key, "[elderberry", "(cherry"); expect(range3).toEqual(["elderberry", "date"]); }); @@ -5010,18 +4273,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zrevrangebylex-limit-test"; - // Add members with same score await redis.send("ZADD", [key, "0", "a", "0", "b", "0", "c", "0", "d", "0", "e", "0", "f", "0", "g"]); - // Get first 3 members in reverse order const limited1 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "0", "3"); expect(limited1).toEqual(["g", "f", "e"]); - // Skip first 2, get next 3 const limited2 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "2", "3"); expect(limited2).toEqual(["e", "d", "c"]); - // Get last 2 with large offset const limited3 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "5", "10"); expect(limited3).toEqual(["b", "a"]); }); @@ -5031,14 +4290,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const source = "zrangestore-source"; const dest = "zrangestore-dest"; - // Add members with scores await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Store members by rank (index 1 to 3) const count1 = await redis.zrangestore(dest, source, 1, 3); expect(count1).toBe(3); - // Verify stored members const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); expect(stored).toEqual(["two", "three", "four"]); }); @@ -5048,14 +4304,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const source = "zrangestore-byscore-source"; const dest = "zrangestore-byscore-dest"; - // Add members await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Store members with score 2-4 (inclusive) const count = await redis.zrangestore(dest, source, "2", "4", "BYSCORE"); expect(count).toBe(3); - // Verify stored members const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["two", 2], @@ -5069,14 +4322,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const source = "zrangestore-rev-source"; const dest = "zrangestore-rev-dest"; - // Add members await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three"]); - // Store in reverse order const count = await redis.zrangestore(dest, source, "0", "-1", "REV"); expect(count).toBe(3); - // Verify stored members (they maintain their scores but were selected in reverse) const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); expect(stored).toEqual(["one", "two", "three"]); }); @@ -5086,14 +4336,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const source = "zrangestore-limit-source"; const dest = "zrangestore-limit-dest"; - // Add members await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); - // Store with BYSCORE and LIMIT const count = await redis.zrangestore(dest, source, "-inf", "+inf", "BYSCORE", "LIMIT", "1", "2"); expect(count).toBe(2); - // Verify stored members (skip first, get next 2) const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); expect(stored).toEqual(["two", "three"]); }); @@ -5130,15 +4377,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "zinter-test-1"; const key2 = "zinter-test-2"; - // Set up sorted sets await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); - // Basic intersection - returns members that exist in all sets const result1 = await redis.zinter(2, key1, key2); expect(result1).toEqual(["b", "c"]); - // With WITHSCORES (scores are summed by default) const result2 = await redis.zinter(2, key1, key2, "WITHSCORES"); expect(result2).toEqual([ ["b", 3], @@ -5151,11 +4395,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "zinter-weights-1"; const key2 = "zinter-weights-2"; - // Set up sorted sets await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); - // With weights (multiply scores) const result = await redis.zinter(2, key1, key2, "WEIGHTS", "2", "3", "WITHSCORES"); expect(result).toEqual([ ["b", 7], @@ -5168,18 +4410,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "zinter-agg-1"; const key2 = "zinter-agg-2"; - // Set up sorted sets await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); - // With MIN aggregation const result1 = await redis.zinter(2, key1, key2, "AGGREGATE", "MIN", "WITHSCORES"); expect(result1).toEqual([ ["b", 1], ["c", 2], ]); - // With MAX aggregation const result2 = await redis.zinter(2, key1, key2, "AGGREGATE", "MAX", "WITHSCORES"); expect(result2).toEqual([ ["b", 2], @@ -5192,11 +4431,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "zinter-empty-1"; const key2 = "zinter-empty-2"; - // Set up sorted sets with no common members await redis.zadd(key1, "1", "a", "2", "b"); await redis.zadd(key2, "1", "c", "2", "d"); - // Empty intersection const result = await redis.zinter(2, key1, key2); expect(result).toEqual([]); }); @@ -5207,15 +4444,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zinterstore-test-2"; const dest = "zinterstore-dest"; - // Set up sorted sets await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); - // Basic intersection store const count = await redis.zinterstore(dest, 2, key1, key2); expect(count).toBe(2); - // Verify stored members const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["b", 3], @@ -5229,15 +4463,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zinterstore-weights-2"; const dest = "zinterstore-weights-dest"; - // Set up sorted sets await redis.zadd(key1, "1", "x", "2", "y"); await redis.zadd(key2, "2", "x", "3", "y"); - // With weights const count = await redis.zinterstore(dest, 2, key1, key2, "WEIGHTS", "2", "3"); expect(count).toBe(2); - // Verify stored members with weighted scores const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["x", 8], @@ -5252,11 +4483,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const destMin = "zinterstore-agg-min"; const destMax = "zinterstore-agg-max"; - // Set up sorted sets await redis.zadd(key1, "1", "m", "3", "n"); await redis.zadd(key2, "2", "m", "1", "n"); - // With MIN aggregation const count1 = await redis.zinterstore(destMin, 2, key1, key2, "AGGREGATE", "MIN"); expect(count1).toBe(2); const storedMin = await redis.send("ZRANGE", [destMin, "0", "-1", "WITHSCORES"]); @@ -5265,7 +4494,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { ["n", 1], ]); - // With MAX aggregation const count2 = await redis.zinterstore(destMax, 2, key1, key2, "AGGREGATE", "MAX"); expect(count2).toBe(2); const storedMax = await redis.send("ZRANGE", [destMax, "0", "-1", "WITHSCORES"]); @@ -5281,15 +4509,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zinterstore-empty-2"; const dest = "zinterstore-empty-dest"; - // Set up sorted sets with no common members await redis.zadd(key1, "1", "a", "2", "b"); await redis.zadd(key2, "1", "c", "2", "d"); - // Empty intersection const count = await redis.zinterstore(dest, 2, key1, key2); expect(count).toBe(0); - // Verify destination is empty const exists = await redis.exists(dest); expect(exists).toBe(false); }); @@ -5299,15 +4524,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "zunion-test-1"; const key2 = "zunion-test-2"; - // Set up sorted sets await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "4", "b", "5", "c", "6", "d"); - // Basic union - returns all members from both sets const result1 = await redis.zunion(2, key1, key2); expect(result1).toEqual(["a", "b", "d", "c"]); - // With WITHSCORES (scores are summed by default) const result2 = await redis.zunion(2, key1, key2, "WITHSCORES"); expect(result2).toEqual([ ["a", 1], @@ -5322,11 +4544,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "zunion-weights-1"; const key2 = "zunion-weights-2"; - // Set up sorted sets await redis.zadd(key1, "1", "x", "2", "y", "3", "z"); await redis.zadd(key2, "2", "y", "3", "z", "4", "w"); - // With weights - multiply scores before aggregation const result = await redis.zunion(2, key1, key2, "WEIGHTS", "2", "3", "WITHSCORES"); expect(result).toEqual([ ["x", 2], @@ -5341,11 +4561,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "zunion-min-1"; const key2 = "zunion-min-2"; - // Set up sorted sets await redis.zadd(key1, "1", "p", "3", "q"); await redis.zadd(key2, "2", "p", "1", "q"); - // With MIN aggregation - take minimum score const result = await redis.zunion(2, key1, key2, "AGGREGATE", "MIN", "WITHSCORES"); expect(result).toEqual([ ["p", 1], @@ -5358,11 +4576,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "zunion-max-1"; const key2 = "zunion-max-2"; - // Set up sorted sets await redis.zadd(key1, "1", "r", "3", "s"); await redis.zadd(key2, "2", "r", "1", "s"); - // With MAX aggregation - take maximum score const result = await redis.zunion(2, key1, key2, "AGGREGATE", "MAX", "WITHSCORES"); expect(result).toEqual([ ["r", 2], @@ -5374,10 +4590,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zunion-single"; - // Set up single sorted set await redis.zadd(key, "1", "one", "2", "two", "3", "three"); - // Union of single set const result = await redis.zunion(1, key); expect(result).toEqual(["one", "two", "three"]); }); @@ -5388,12 +4602,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zunion-three-2"; const key3 = "zunion-three-3"; - // Set up three sorted sets await redis.zadd(key1, "1", "a", "2", "b"); await redis.zadd(key2, "2", "b", "3", "c"); await redis.zadd(key3, "3", "c", "4", "d"); - // Union of three sets const result = await redis.zunion(3, key1, key2, key3, "WITHSCORES"); expect(result).toEqual([ ["a", 1], @@ -5408,10 +4620,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "zunion-empty-1"; const key2 = "zunion-empty-2"; - // Set up one sorted set, leave other empty await redis.zadd(key1, "1", "a", "2", "b"); - // Union with empty set const result = await redis.zunion(2, key1, key2); expect(result).toEqual(["a", "b"]); }); @@ -5422,15 +4632,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zunionstore-test-2"; const dest = "zunionstore-dest"; - // Set up sorted sets await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "4", "b", "5", "c", "6", "d"); - // Basic union store const count = await redis.zunionstore(dest, 2, key1, key2); expect(count).toBe(4); - // Verify stored members const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["a", 1], @@ -5446,15 +4653,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zunionstore-weights-2"; const dest = "zunionstore-weights-dest"; - // Set up sorted sets await redis.zadd(key1, "1", "x", "2", "y"); await redis.zadd(key2, "2", "x", "3", "y"); - // With weights const count = await redis.zunionstore(dest, 2, key1, key2, "WEIGHTS", "2", "3"); expect(count).toBe(2); - // Verify stored members with weighted scores const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["x", 8], @@ -5468,11 +4672,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zunionstore-agg-min-2"; const dest = "zunionstore-agg-min-dest"; - // Set up sorted sets await redis.zadd(key1, "1", "m", "3", "n"); await redis.zadd(key2, "2", "m", "1", "n"); - // With MIN aggregation const count = await redis.zunionstore(dest, 2, key1, key2, "AGGREGATE", "MIN"); expect(count).toBe(2); const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); @@ -5488,11 +4690,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zunionstore-agg-max-2"; const dest = "zunionstore-agg-max-dest"; - // Set up sorted sets await redis.zadd(key1, "1", "m", "3", "n"); await redis.zadd(key2, "2", "m", "1", "n"); - // With MAX aggregation const count = await redis.zunionstore(dest, 2, key1, key2, "AGGREGATE", "MAX"); expect(count).toBe(2); const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); @@ -5508,18 +4708,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zunionstore-overwrite-2"; const dest = "zunionstore-overwrite-dest"; - // Set up initial destination await redis.zadd(dest, "100", "old"); - // Set up sorted sets await redis.zadd(key1, "1", "a", "2", "b"); await redis.zadd(key2, "3", "c"); - // Union store should overwrite const count = await redis.zunionstore(dest, 2, key1, key2); expect(count).toBe(3); - // Verify old member is gone const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); expect(stored).toEqual(["a", "b", "c"]); expect(stored).not.toContain("old"); @@ -5531,11 +4727,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zunionstore-empty-2"; const dest = "zunionstore-empty-dest"; - // Both sets empty const count = await redis.zunionstore(dest, 2, key1, key2); expect(count).toBe(0); - // Verify destination is empty const exists = await redis.exists(dest); expect(exists).toBe(false); }); @@ -5614,7 +4808,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { ["three", 3], ]); - // Verify remaining count const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); @@ -5623,7 +4816,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const emptyKey = "zmpop-empty-test"; - // Try to pop from empty set const result = await redis.zmpop(1, emptyKey, "MIN"); expect(result).toBeNull(); }); @@ -5634,10 +4826,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key2 = "zmpop-multi-test2"; const key3 = "zmpop-multi-test3"; - // Only populate key2 await redis.send("ZADD", [key2, "1", "one", "2", "two"]); - // Pop from multiple keys (should get from key2) const result = await redis.zmpop(3, key1, key2, key3, "MIN"); expect(result).toEqual([key2, [["one", 1]]]); }); @@ -5646,10 +4836,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "bzmpop-test"; - // Add a member to the set await redis.send("ZADD", [key, "1", "one", "2", "two"]); - // Use short timeout for testing const result = await redis.bzmpop(0.1, 1, key, "MIN"); expect(result).toEqual([key, [["one", 1]]]); @@ -5680,7 +4868,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { ["two", 2], ]); - // Verify one member remains const count = await redis.send("ZCARD", [key]); expect(count).toBe(1); }); @@ -5703,10 +4890,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zpopmax-test"; - // Add members to sorted set await redis.zadd(key, 1.0, "one", 2.0, "two", 3.0, "three"); - // Pop highest score const result = await redis.zpopmax(key); expect(result).toBeDefined(); expect(result).not.toBeNull(); @@ -5715,7 +4900,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(result![0]).toBe("three"); expect(result![1]).toBe(3); - // Verify member was removed const count = await redis.zcard(key); expect(count).toBe(2); }); @@ -5724,10 +4908,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "zpopmin-test"; - // Add members to sorted set await redis.zadd(key, 1.0, "one", 2.0, "two", 3.0, "three"); - // Pop lowest score const result = await redis.zpopmin(key); expect(result).toBeDefined(); expect(result).not.toBeNull(); @@ -5736,7 +4918,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(result![0]).toBe("one"); expect(result![1]).toBe(1); - // Verify member was removed const count = await redis.zcard(key); expect(count).toBe(2); }); @@ -5761,10 +4942,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "bzpopmin-test"; - // Add members to sorted set await redis.send("ZADD", [key, "1.0", "one", "2.0", "two", "3.0", "three"]); - // Pop lowest score with short timeout const result = await redis.bzpopmin(key, 0.1); expect(result).toBeDefined(); expect(result).toHaveLength(3); @@ -5772,7 +4951,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(result![1]).toBe("one"); expect(result![2]).toBe(1); - // Verify member was removed const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); @@ -5781,7 +4959,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "bzpopmin-empty-test"; - // Try to pop from non-existent key with short timeout const result = await redis.bzpopmin(key, 0.1); expect(result).toBeNull(); }); @@ -5790,10 +4967,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "bzpopmax-test"; - // Add members to sorted set await redis.send("ZADD", [key, "1.0", "one", "2.0", "two", "3.0", "three"]); - // Pop highest score with short timeout const result = await redis.bzpopmax(key, 0.1); expect(result).toBeDefined(); expect(result).toHaveLength(3); @@ -5801,7 +4976,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(result![1]).toBe("three"); expect(result![2]).toBe(3); - // Verify member was removed const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); @@ -5810,7 +4984,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "bzpopmax-empty-test"; - // Try to pop from non-existent key with short timeout const result = await redis.bzpopmax(key, 0.1); expect(result).toBeNull(); }); @@ -5820,10 +4993,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "bzpopmin-multi-1"; const key2 = "bzpopmin-multi-2"; - // Add members to second key only await redis.send("ZADD", [key2, "5.0", "five", "6.0", "six"]); - // Pop from multiple keys (should return from key2) const result = await redis.bzpopmin(key1, key2, 0.1); expect(result).toBeDefined(); expect(result![0]).toBe(key2); @@ -5836,10 +5007,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key1 = "bzpopmax-multi-1"; const key2 = "bzpopmax-multi-2"; - // Add members to second key only await redis.send("ZADD", [key2, "5.0", "five", "6.0", "six"]); - // Pop from multiple keys (should return from key2) const result = await redis.bzpopmax(key1, key2, 0.5); expect(result).toBeDefined(); expect(result![0]).toBe(key2); @@ -5871,15 +5040,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "hincrby-test"; - // Increment non-existent field (creates it) const val1 = await redis.hincrby(key, "field1", 5); expect(val1).toBe(5); - // Increment existing field const val2 = await redis.hincrby(key, "field1", 3); expect(val2).toBe(8); - // Decrement const val3 = await redis.hincrby(key, "field1", -2); expect(val3).toBe(6); }); @@ -5902,11 +5068,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "hkeys-test"; - // Empty hash const keys1 = await redis.hkeys(key); expect(keys1).toEqual([]); - // Add fields await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); const keys2 = await redis.hkeys(key); @@ -5917,16 +5081,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "hlen-test"; - // Empty hash const len1 = await redis.hlen(key); expect(len1).toBe(0); - // Add fields await redis.hset(key, "field1", "value1", "field2", "value2"); const len2 = await redis.hlen(key); expect(len2).toBe(2); - // Add more await redis.hset(key, "field3", "value3"); const len3 = await redis.hlen(key); expect(len3).toBe(3); @@ -5938,11 +5099,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); - // Get multiple fields const values = await redis.hmget(key, "field1", "field2", "field3"); expect(values).toEqual(["value1", "value2", "value3"]); - // Mix existing and non-existing const mixed = await redis.hmget(key, "field1", "nonexistent", "field2"); expect(mixed).toEqual(["value1", null, "value2"]); }); @@ -5951,11 +5110,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "hvals-test"; - // Empty hash const vals1 = await redis.hvals(key); expect(vals1).toEqual([]); - // Add fields await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); const vals2 = await redis.hvals(key); @@ -5974,7 +5131,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const len2 = await redis.hstrlen(key, "field2"); expect(len2).toBe(6); - // Non-existent field const len3 = await redis.hstrlen(key, "nonexistent"); expect(len3).toBe(0); }); @@ -5985,11 +5141,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, "field1", "value1", "field2", "value2"); - // Set expiration on field1 const result = await redis.hexpire(key, 60, "FIELDS", 1, "field1"); expect(result).toEqual([1]); - // Verify TTL is set const ttl = await redis.httl(key, "FIELDS", 1, "field1"); expect(ttl[0]).toBeGreaterThan(0); expect(ttl[0]).toBeLessThanOrEqual(60); @@ -6030,15 +5184,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, "field1", "value1"); await redis.hexpire(key, 60, "FIELDS", 1, "field1"); - // Verify expiration is set const ttlBefore = await redis.httl(key, "FIELDS", 1, "field1"); expect(ttlBefore[0]).toBeGreaterThan(0); - // Remove expiration const result = await redis.hpersist(key, "FIELDS", 1, "field1"); expect(result).toEqual([1]); - // Verify no expiration const ttlAfter = await redis.httl(key, "FIELDS", 1, "field1"); expect(ttlAfter[0]).toBe(-1); }); @@ -6076,7 +5227,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "hash-object-test"; const result = await redis.hset(key, { field1: "value1", field2: "value2", field3: "value3" }); - expect(result).toBe(3); // 3 new fields added + expect(result).toBe(3); const value1 = await redis.hget(key, "field1"); expect(value1).toBe("value1"); @@ -6091,7 +5242,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "hash-variadic-test"; const result = await redis.hset(key, "field1", "value1", "field2", "value2"); - expect(result).toBe(2); // 2 new fields added + expect(result).toBe(2); const value1 = await redis.hget(key, "field1"); expect(value1).toBe("value1"); @@ -6104,7 +5255,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const key = "hash-single-test"; const result = await redis.hset(key, "field1", "value1"); - expect(result).toBe(1); // 1 new field added + expect(result).toBe(1); const value = await redis.hget(key, "field1"); expect(value).toBe("value1"); @@ -6114,13 +5265,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "hash-update-test"; - // Initial set const result1 = await redis.hset(key, { field1: "value1", field2: "value2" }); expect(result1).toBe(2); - // Update existing and add new const result2 = await redis.hset(key, { field1: "new-value1", field3: "value3" }); - expect(result2).toBe(1); // Only field3 is new + expect(result2).toBe(1); const value1 = await redis.hget(key, "field1"); expect(value1).toBe("new-value1"); @@ -6415,7 +5564,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); - const futureTimestamp = Math.floor(Date.now() / 1000) + 60; // 60 seconds from now + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const result = await redis.hsetex(key, "EXAT", futureTimestamp, "FIELDS", 1, "name", "John"); expect(result).toBe(1); @@ -6426,7 +5575,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); - const futureTimestamp = Date.now() + 60000; // 60 seconds from now + const futureTimestamp = Date.now() + 60000; const result = await redis.hsetex(key, "PXAT", futureTimestamp, "FIELDS", 1, "name", "John"); expect(result).toBe(1); @@ -6640,9 +5789,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const [cursor, fields] = await redis.hscan(key, 0); expect(typeof cursor).toBe("string"); expect(fields).toBeInstanceOf(Array); - expect(fields.length).toBe(6); // [field1, value1, field2, value2, field3, value3] + expect(fields.length).toBe(6); - // Convert to object for easier testing const obj: Record = {}; for (let i = 0; i < fields.length; i += 2) { obj[fields[i]] = fields[i + 1]; @@ -6660,13 +5808,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(typeof cursor).toBe("string"); expect(fields).toBeInstanceOf(Array); - // Convert to object const obj: Record = {}; for (let i = 0; i < fields.length; i += 2) { obj[fields[i]] = fields[i + 1]; } - // Should only contain fields matching "field*" expect(obj.field1).toBe("val1"); expect(obj.field2).toBe("val2"); expect(obj.other).toBeUndefined(); @@ -6676,7 +5822,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); - // Add many fields const fields: Record = {}; for (let i = 0; i < 20; i++) { fields[`field${i}`] = `value${i}`; @@ -6686,7 +5831,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const [cursor, result] = await redis.hscan(key, 0, "COUNT", 5); expect(typeof cursor).toBe("string"); expect(result).toBeInstanceOf(Array); - // COUNT is a hint, so we just check we got some results + expect(result.length).toBeGreaterThan(0); }); @@ -6837,10 +5982,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, { name: "John", description: "Software Engineer" }); const nameLen = await redis.hstrlen(key, "name"); - expect(nameLen).toBe(4); // "John" has 4 characters + expect(nameLen).toBe(4); const descLen = await redis.hstrlen(key, "description"); - expect(descLen).toBe(17); // "Software Engineer" has 17 characters + expect(descLen).toBe(17); }); test("should return 0 for non-existent field using hstrlen", async () => { @@ -6867,7 +6012,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(ttls[0]).toBeLessThanOrEqual(10); expect(ttls[1]).toBeGreaterThan(0); expect(ttls[1]).toBeLessThanOrEqual(10); - expect(ttls[2]).toBe(-1); // city has no expiration + expect(ttls[2]).toBe(-1); }); test("should expire hash fields with NX flag using hexpire", async () => { @@ -6878,7 +6023,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, { age: "30" }); const result = await redis.hexpire(key, 10, "NX", "FIELDS", 2, "name", "age"); - expect(result).toEqual([0, 1]); // name already has TTL (NX fails), age doesn't (NX succeeds) + expect(result).toEqual([0, 1]); }); test("should expire hash fields at specific time using hexpireat", async () => { @@ -6887,7 +6032,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, { name: "John", age: "30" }); - const futureTimestamp = Math.floor(Date.now() / 1000) + 60; // 60 seconds from now + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const result = await redis.hexpireat(key, futureTimestamp, "FIELDS", 2, "name", "age"); expect(result).toEqual([1, 1]); @@ -6911,7 +6056,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(expiretimes).toHaveLength(2); expect(expiretimes[0]).toBeGreaterThan(0); expect(expiretimes[0]).toBeLessThanOrEqual(futureTimestamp); - expect(expiretimes[1]).toBe(-1); // age has no expiration + expect(expiretimes[1]).toBe(-1); }); test("should expire hash fields at specific time in milliseconds using hpexpireat", async () => { @@ -6920,15 +6065,15 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, { name: "John", age: "30" }); - const futureTimestamp = Date.now() + 60000; // 60 seconds from now + const futureTimestamp = Date.now() + 60000; const result = await redis.hpexpireat(key, futureTimestamp, "FIELDS", 2, "name", "age"); expect(result).toEqual([1, 1]); const ttls = await redis.hpttl(key, "FIELDS", 2, "name", "age"); expect(ttls[0]).toBeGreaterThan(0); - expect(ttls[0]).toBeLessThanOrEqual(60100); // Small buffer for timing + expect(ttls[0]).toBeLessThanOrEqual(60100); expect(ttls[1]).toBeGreaterThan(0); - expect(ttls[1]).toBeLessThanOrEqual(60100); // Small buffer for timing + expect(ttls[1]).toBeLessThanOrEqual(60100); }); test("should get hash field expiration time in milliseconds using hpexpiretime", async () => { @@ -6944,7 +6089,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(expiretimes).toHaveLength(2); expect(expiretimes[0]).toBeGreaterThan(0); expect(expiretimes[0]).toBeLessThanOrEqual(futureTimestamp); - expect(expiretimes[1]).toBe(-1); // age has no expiration + expect(expiretimes[1]).toBe(-1); }); test("should persist hash fields using hpersist", async () => { @@ -6954,10 +6099,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hsetex(key, "EX", 100, "FIELDS", 2, "name", "John", "age", "30"); const result = await redis.hpersist(key, "FIELDS", 2, "name", "age"); - expect(result).toEqual([1, 1]); // Both fields had TTL and were persisted + expect(result).toEqual([1, 1]); const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); - expect(ttls).toEqual([-1, -1]); // Both fields now have no expiration + expect(ttls).toEqual([-1, -1]); }); test("should return 0 for fields without expiration using hpersist", async () => { @@ -6967,14 +6112,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await redis.hset(key, { name: "John", age: "30" }); const result = await redis.hpersist(key, "FIELDS", 2, "name", "age"); - expect(result).toEqual([-1, -1]); // Neither field had TTL (returns -1 for no TTL) + expect(result).toEqual([-1, -1]); }); }); describe("Connection State", () => { test("should have a connected property", () => { const redis = ctx.redis; - // The client should expose a connected property + expect(typeof redis.connected).toBe("boolean"); }); }); @@ -6982,16 +6127,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { describe("RESP3 Data Types", () => { test("should handle hash maps (dictionaries) as command responses", async () => { const redis = ctx.redis; - // HSET multiple fields + const userId = "user:" + randomUUIDv7().substring(0, 8); const setResult = await redis.send("HSET", [userId, "name", "John", "age", "30", "active", "true"]); expect(setResult).toBeDefined(); - // HGETALL returns object with key-value pairs const hash = await redis.send("HGETALL", [userId]); expect(hash).toBeDefined(); - // Proper structure checking when RESP3 maps are fixed if (typeof hash === "object" && hash !== null) { expect(hash).toHaveProperty("name"); expect(hash).toHaveProperty("age"); @@ -7005,19 +6148,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should handle sets as command responses", async () => { const redis = ctx.redis; - // Add items to a set + const setKey = "colors:" + randomUUIDv7().substring(0, 8); const addResult = await redis.send("SADD", [setKey, "red", "blue", "green"]); expect(addResult).toBeDefined(); - // Get set members const setMembers = await redis.send("SMEMBERS", [setKey]); expect(setMembers).toBeDefined(); - // Check if the response is an array expect(Array.isArray(setMembers)).toBe(true); - // Should contain our colors expect(setMembers).toContain("red"); expect(setMembers).toContain("blue"); expect(setMembers).toContain("green"); @@ -7042,7 +6182,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test.each([...Array(16).keys()])("Connecting to database with url $url succeeds", async (dbId: number) => { const redis = createClient(connectionType, {}, dbId); - // Ensure the value is not in the database. const testValue = await redis.get(testKeyUniquePerDb); expect(testValue).toBeNull(); @@ -7052,14 +6191,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { describe("Reconnections", () => { test.skip("should automatically reconnect after connection drop", async () => { - // NOTE: This test was already broken before the Docker Compose migration. - // It times out after 31 seconds with "Max reconnection attempts reached" - // This appears to be an issue with the Redis client's automatic reconnection - // behavior, not related to the Docker infrastructure changes. const TEST_KEY = "test-key"; const TEST_VALUE = "test-value"; - // Ensure we have a working client to start if (!ctx.redis || !ctx.redis.connected) { ctx.redis = createClient(connectionType); } @@ -7067,7 +6201,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const valueBeforeStart = await ctx.redis.get(TEST_KEY); expect(valueBeforeStart).toBeNull(); - // Set some value await ctx.redis.set(TEST_KEY, TEST_VALUE); const valueAfterSet = await ctx.redis.get(TEST_KEY); expect(valueAfterSet).toBe(TEST_VALUE); @@ -7095,7 +6228,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }; beforeEach(async () => { - // The PUB/SUB tests expect that ctx.redis is connected but not in subscriber mode. await ctx.cleanupSubscribers(); }); @@ -7203,10 +6335,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await counter.untilValue(TEST_MESSAGE_COUNT); - // Check that we received messages on both channels expect(Object.keys(receivedMessages).sort()).toEqual(Object.keys(sentMessages).sort()); - // Check messages match for each channel for (const channel of channels) { if (sentMessages[channel]) { expect(receivedMessages[channel]).toEqual(sentMessages[channel]); @@ -7226,35 +6356,29 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { let receivedMessages: { [channel: string]: string[] } = {}; - // Total counter for all messages we expect to receive: 3 initial + 2 after unsubscribe = 5 total const counter = awaitableCounter(); - // Subscribe to three channels await subscriber.subscribe([channel1, channel2, channel3], (message, channel) => { receivedMessages[channel] = receivedMessages[channel] || []; receivedMessages[channel].push(message); counter.increment(); }); - // Send initial messages to all channels expect(await ctx.redis.publish(channel1, "msg1-before")).toBe(1); expect(await ctx.redis.publish(channel2, "msg2-before")).toBe(1); expect(await ctx.redis.publish(channel3, "msg3-before")).toBe(1); - // Wait for initial messages, then unsubscribe from channel2 await counter.untilValue(3); await subscriber.unsubscribe(channel2); - // Send messages after unsubscribing from channel2 expect(await ctx.redis.publish(channel1, "msg1-after")).toBe(1); expect(await ctx.redis.publish(channel2, "msg2-after")).toBe(0); expect(await ctx.redis.publish(channel3, "msg3-after")).toBe(1); await counter.untilValue(5); - // Check we received messages only on subscribed channels expect(receivedMessages[channel1]).toEqual(["msg1-before", "msg1-after"]); - expect(receivedMessages[channel2]).toEqual(["msg2-before"]); // No "msg2-after" + expect(receivedMessages[channel2]).toEqual(["msg2-before"]); expect(receivedMessages[channel3]).toEqual(["msg3-before", "msg3-after"]); await subscriber.unsubscribe([channel1, channel3]); @@ -7279,16 +6403,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { counter.increment(); }; - // Subscribe to the same channel twice await subscriber.subscribe(channel, listener); await subscriber.subscribe(channel, listener2); - // Publish a single message expect(await ctx.redis.publish(channel, "test-message")).toBe(1); await counter.untilValue(2); - // Both listeners should have been called once. expect(callCount).toBe(1); expect(callCount2).toBe(1); @@ -7350,7 +6471,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); - // Ping should work in subscription mode const pong = await subscriber.ping(); expect(pong).toBe("PONG"); @@ -7364,7 +6484,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); - // Publishing from the same client should work expect(async () => subscriber.publish(channel, "self-published")).toThrow( "RedisClient.prototype.publish cannot be called while in subscriber mode.", ); @@ -7377,15 +6496,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); - // Should fail in subscription mode expect(() => subscriber.set(testKey, testValue())).toThrow( "RedisClient.prototype.set cannot be called while in subscriber mode.", ); - // Unsubscribe from all channels await subscriber.unsubscribe(); - // Should work after unsubscribing const result = await ctx.redis.set(testKey, "value"); expect(result).toBe("OK"); @@ -7396,7 +6512,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("publishing without subscribers succeeds", async () => { const channel = "no-subscribers-channel"; - // Publishing without subscribers should not throw expect(await ctx.redis.publish(channel, "message")).toBe(0); }); @@ -7416,7 +6531,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const STEP_SECOND_MESSAGE = 3; const STEP_THIRD_MESSAGE = 4; - // stepCounter is a slight hack to track the progress of the subprocess. const stepCounter = awaitableCounter(); let currentMessage: any = {}; @@ -7444,19 +6558,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await stepCounter.untilValue(STEP_SUBSCRIBED); expect(currentMessage.event).toBe("ready"); - // Send multiple messages expect(await ctx.redis.publish(channel, "message1")).toBeGreaterThanOrEqual(1); await stepCounter.untilValue(STEP_FIRST_MESSAGE); expect(currentMessage.event).toBe("message"); expect(currentMessage.index).toBe(1); - // Now, the subscriber process will crash expect(await ctx.redis.publish(channel, "message2")).toBeGreaterThanOrEqual(1); await stepCounter.untilValue(STEP_SECOND_MESSAGE); expect(currentMessage.event).toBe("exception"); //expect(currentMessage.index).toBe(2); - // But it should recover and continue receiving messages expect(await ctx.redis.publish(channel, "message3")).toBeGreaterThanOrEqual(1); await stepCounter.untilValue(STEP_THIRD_MESSAGE); expect(currentMessage.event).toBe("message"); @@ -7481,7 +6592,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const subscriber = createClient(connectionType); await subscriber.connect(); - // First phase: both listeners should receive 1 message each (2 total) const counter = awaitableCounter(); let messageCount1 = 0; const listener1 = () => { @@ -7521,7 +6631,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(duplicate.connected).toBe(true); expect(duplicate).not.toBe(ctx.redis); - // Both should work independently await ctx.redis.set("test-original", "original-value"); await duplicate.set("test-duplicate", "duplicate-value"); @@ -7536,7 +6645,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const duplicate = await ctx.redis.duplicate(); - // Both clients should be able to perform the same operations const testKey = `duplicate-config-test-${randomUUIDv7().substring(0, 8)}`; const testValue = "test-value"; @@ -7551,7 +6659,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should allow duplicate to work independently from original", async () => { const duplicate = await ctx.redis.duplicate(); - // Close original, duplicate should still work duplicate.close(); const testKey = `independent-test-${randomUUIDv7().substring(0, 8)}`; @@ -7568,12 +6675,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const testChannel = "test-subscriber-duplicate"; - // Put original client in subscriber mode await subscriber.subscribe(testChannel, () => {}); const duplicate = await subscriber.duplicate(); - // Duplicate should not be in subscriber mode expect(() => duplicate.set("test-key", "test-value")).not.toThrow(); await subscriber.unsubscribe(testChannel); @@ -7586,12 +6691,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const duplicate2 = await ctx.redis.duplicate(); const duplicate3 = await ctx.redis.duplicate(); - // All should be connected expect(duplicate1.connected).toBe(true); expect(duplicate2.connected).toBe(true); expect(duplicate3.connected).toBe(true); - // All should work independently const testKey = `multi-duplicate-test-${randomUUIDv7().substring(0, 8)}`; await duplicate1.set(`${testKey}-1`, "value-1"); await duplicate2.set(`${testKey}-2`, "value-2"); @@ -7601,7 +6704,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(await duplicate2.get(`${testKey}-2`)).toBe("value-2"); expect(await duplicate3.get(`${testKey}-3`)).toBe("value-3"); - // Cross-check: each duplicate can read what others wrote expect(await duplicate1.get(`${testKey}-2`)).toBe("value-2"); expect(await duplicate2.get(`${testKey}-3`)).toBe("value-3"); expect(await duplicate3.get(`${testKey}-1`)).toBe("value-1"); @@ -7612,7 +6714,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); test("should duplicate client that failed to connect", async () => { - // Create client with invalid credentials to force connection failure const url = new URL(connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL); url.username = "invaliduser"; url.password = "invalidpassword"; @@ -7620,7 +6721,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { tls: connectionType === ConnectionType.TLS ? TLS_REDIS_OPTIONS.tls : false, }); - // Try to connect and expect it to fail let connectionFailed = false; try { await failedRedis.connect(); @@ -7631,7 +6731,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(connectionFailed).toBe(true); expect(failedRedis.connected).toBe(false); - // Duplicate should also remain unconnected const duplicate = await failedRedis.duplicate(); expect(duplicate.connected).toBe(false); }); @@ -7639,17 +6738,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should handle duplicate timing with concurrent operations", async () => { await ctx.redis.connect(); - // Start some operations on the original client const testKey = `concurrent-test-${randomUUIDv7().substring(0, 8)}`; const originalOperation = ctx.redis.set(testKey, "original-value"); - // Create duplicate while operation is in flight const duplicate = await ctx.redis.duplicate(); - // Wait for original operation to complete await originalOperation; - // Duplicate should be able to read the value expect(await duplicate.get(testKey)).toBe("original-value"); duplicate.close(); From c9fa18b3de62e201143e0bfe7e2933c2e23f4f1c Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 05:05:39 -0700 Subject: [PATCH 41/45] tests for hmget(key, string_arr) --- packages/bun-types/redis.d.ts | 8 ++++++++ test/js/valkey/valkey.test.ts | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 0d71188a110b..b1367aa9fc29 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -633,6 +633,14 @@ declare module "bun" { */ hmget(key: RedisClient.KeyLike, ...fields: string[]): Promise>; + /** + * Get the values of all the given hash fields + * @param key The hash key + * @param fields The fields to get + * @returns Promise that resolves with an array of values + */ + hmget(key: RedisClient.KeyLike, fields: string[]): Promise>; + /** * Delete one or more hash fields * @param key The hash key diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 6c40be2f2c32..9ae92669e904 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -5093,6 +5093,19 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(len3).toBe(3); }); + test("should get multiple hash values with HMGET where only two arguments are passed because the second is an array", async () => { + const redis = ctx.redis; + const key = "hmget-test"; + + await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); + + const values = await redis.hmget(key, ["field1", "field2", "field3"]); + expect(values).toEqual(["value1", "value2", "value3"]); + + const mixed = await redis.hmget(key, ["field1", "nonexistent", "field2"]); + expect(mixed).toEqual(["value1", null, "value2"]); + }); + test("should get multiple hash values with HMGET", async () => { const redis = ctx.redis; const key = "hmget-test"; From e65fbc391ce600172202a704f1bee589a980d523 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 05:39:39 -0700 Subject: [PATCH 42/45] fix: support hmget with an array of fields as the second arg --- src/valkey/js_valkey_functions.zig | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index f91aaf9ca2e4..c8bd4b107874 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -528,14 +528,30 @@ pub fn hmget(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe }; args.appendAssumeCapacity(key); - for (args_view[1..]) |arg| { - if (arg.isUndefinedOrNull()) { - break; + const second_arg = callframe.argument(1); + if (second_arg.isArray()) { + const array_len = try second_arg.getLength(globalObject); + if (array_len == 0) { + return globalObject.throw("HMGET requires at least one field", .{}); + } + + var array_iter = try second_arg.arrayIterator(globalObject); + while (try array_iter.next()) |element| { + const field = (try fromJS(globalObject, element)) orelse { + return globalObject.throwInvalidArgumentType("hmget", "field", "string or buffer"); + }; + try args.append(field); + } + } else { + for (args_view[1..]) |arg| { + if (arg.isUndefinedOrNull()) { + break; + } + const field = (try fromJS(globalObject, arg)) orelse { + return globalObject.throwInvalidArgumentType("hmget", "field", "string or buffer"); + }; + try args.append(field); } - const field = (try fromJS(globalObject, arg)) orelse { - return globalObject.throwInvalidArgumentType("hmget", "field", "string or buffer"); - }; - args.appendAssumeCapacity(field); } // Send HMGET command From 7e18d04b0b475d5238d5c52a1f76b3fc3f30c172 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 05:45:57 -0700 Subject: [PATCH 43/45] srandmember has no withscores (not sorted set) --- src/valkey/js_valkey_functions.zig | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index c8bd4b107874..c3f4f9e1d5e5 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -342,14 +342,6 @@ pub fn srandmember(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, cal return globalObject.throwInvalidArgumentType("srandmember", "count", "number or string"); }; args.appendAssumeCapacity(count_arg); - - // Optional WITHSCORES argument - if (args_view.len > 2 and !callframe.argument(2).isUndefinedOrNull()) { - const withscores_arg = try fromJS(globalObject, callframe.argument(2)) orelse { - return globalObject.throwInvalidArgumentType("srandmember", "withscores", "string"); - }; - args.appendAssumeCapacity(withscores_arg); - } } // Send SRANDMEMBER command From eeddcd4a4e21b4ad9ce525962e4f9066f470fa83 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 05:46:49 -0700 Subject: [PATCH 44/45] expect error for invalid args --- test/js/valkey/valkey.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 9ae92669e904..fea9e466a993 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -2517,6 +2517,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { + // @ts-expect-error await redis.sinter(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sinter' command"`); }); @@ -2525,6 +2526,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { + // @ts-expect-error await redis.sinterstore(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sinterstore' command"`); }); @@ -2533,6 +2535,7 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const redis = ctx.redis; expect(async () => { + // @ts-expect-error await redis.sintercard(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sintercard' command"`); }); From b2bced7f85719f51ea3db192a1b9a1169596a680 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 30 Sep 2025 06:02:57 -0700 Subject: [PATCH 45/45] Fix some return types --- packages/bun-types/redis.d.ts | 4 ++-- test/js/valkey/valkey.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index b1367aa9fc29..3011e66c1c51 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -876,9 +876,9 @@ declare module "bun" { /** * Get all the fields and values in a hash * @param key The hash key - * @returns Promise that resolves with an object containing all fields and values + * @returns Promise that resolves with an object containing all fields and values, or empty object if key does not exist */ - hgetall(key: RedisClient.KeyLike): Promise | null>; + hgetall(key: RedisClient.KeyLike): Promise>; /** * Get all field names in a hash diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index fea9e466a993..d69ac7d6d6d0 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -3973,6 +3973,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(result).toEqual([]); }); + test("should return empty array when ZPOPMIN on non-existent key", async () => { + const redis = ctx.redis; + const nonExistentKey = "zpopmin-nonexistent-" + randomUUIDv7(); + + const result = await redis.zpopmin(nonExistentKey); + expect(result).toEqual([]); + }); + test("should handle ties in score with ZPOPMIN", async () => { const redis = ctx.redis; const key = "zpopmin-tie-test"; @@ -4030,6 +4038,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(result).toEqual([]); }); + test("should return empty array when ZPOPMAX on non-existent key", async () => { + const redis = ctx.redis; + const nonExistentKey = "zpopmax-nonexistent-" + randomUUIDv7(); + const result = await redis.zpopmax(nonExistentKey); + expect(result).toEqual([]); + }); + test("should handle ties in score with ZPOPMAX", async () => { const redis = ctx.redis; const key = "zpopmax-tie-test"; @@ -5739,6 +5754,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(remaining).toEqual({ name: "John", country: "USA" }); }); + test("should return empty object for hgetall on non-existent key", async () => { + const redis = ctx.redis; + const key = "nonexistent-hgetall-" + randomUUIDv7(); + const result = await redis.hgetall(key); + expect(result).toEqual({}); + }); + test("should check if hash field exists using hexists", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8);