diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fed2211b..05012ad626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) +* Node: Added BITOP command ([#2012](https://github.com/valkey-io/valkey-glide/pull/2012)) * Node: Added GETBIT command ([#1989](https://github.com/valkey-io/valkey-glide/pull/1989)) * Node: Added SETBIT command ([#1978](https://github.com/valkey-io/valkey-glide/pull/1978)) * Node: Added LPUSHX and RPUSHX command([#1959](https://github.com/valkey-io/valkey-glide/pull/1959)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index d4b45fe370..c74dc3bc4c 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -76,6 +76,7 @@ function initialize() { const { BitOffsetOptions, BitmapIndexType, + BitwiseOperation, ConditionalChange, GeoAddOptions, GeospatialData, @@ -130,6 +131,7 @@ function initialize() { module.exports = { BitOffsetOptions, BitmapIndexType, + BitwiseOperation, ConditionalChange, GeoAddOptions, GeospatialData, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 8adeaa8552..c7caf83c75 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -12,6 +12,7 @@ import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { AggregationType, + BitwiseOperation, ExpireOptions, InsertPosition, KeyWeight, @@ -28,6 +29,7 @@ import { createBLPop, createBRPop, createBitCount, + createBitOp, createDecr, createDecrBy, createDel, @@ -979,6 +981,39 @@ export class BaseClient { return this.createWritePromise(createDecrBy(key, amount)); } + /** + * Perform a bitwise operation between multiple keys (containing string values) and store the result in the + * `destination`. + * + * See https://valkey.io/commands/bitop/ for more details. + * + * @remarks When in cluster mode, `destination` and all `keys` must map to the same hash slot. + * @param operation - The bitwise operation to perform. + * @param destination - The key that will store the resulting string. + * @param keys - The list of keys to perform the bitwise operation on. + * @returns The size of the string stored in `destination`. + * + * @example + * ```typescript + * await client.set("key1", "A"); // "A" has binary value 01000001 + * await client.set("key2", "B"); // "B" has binary value 01000010 + * const result1 = await client.bitop(BitwiseOperation.AND, "destination", ["key1", "key2"]); + * console.log(result1); // Output: 1 - The size of the resulting string stored in "destination" is 1. + * + * const result2 = await client.get("destination"); + * console.log(result2); // Output: "@" - "@" has binary value 01000000 + * ``` + */ + public bitop( + operation: BitwiseOperation, + destination: string, + keys: string[], + ): Promise { + return this.createWritePromise( + createBitOp(operation, destination, keys), + ); + } + /** * Returns the bit value at `offset` in the string value stored at `key`. `offset` must be greater than or equal * to zero. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index b34e7be2b1..7c8c0a785b 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -446,6 +446,28 @@ export function createDecrBy( return createCommand(RequestType.DecrBy, [key, amount.toString()]); } +/** + * Enumeration defining the bitwise operation to use in the {@link BaseClient.bitop|bitop} command. Specifies the + * bitwise operation to perform between the passed in keys. + */ +export enum BitwiseOperation { + AND = "AND", + OR = "OR", + XOR = "XOR", + NOT = "NOT", +} + +/** + * @internal + */ +export function createBitOp( + operation: BitwiseOperation, + destination: string, + keys: string[], +): command_request.Command { + return createCommand(RequestType.BitOp, [operation, destination, ...keys]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 8a9b1b61e0..c23b771e46 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -4,6 +4,7 @@ import { AggregationType, + BitwiseOperation, ExpireOptions, InfoOptions, InsertPosition, @@ -22,6 +23,7 @@ import { createBLPop, createBRPop, createBitCount, + createBitOp, createClientGetName, createClientId, createConfigGet, @@ -395,6 +397,26 @@ export class BaseTransaction> { return this.addAndReturn(createDecrBy(key, amount)); } + /** + * Perform a bitwise operation between multiple keys (containing string values) and store the result in the + * `destination`. + * + * See https://valkey.io/commands/bitop/ for more details. + * + * @param operation - The bitwise operation to perform. + * @param destination - The key that will store the resulting string. + * @param keys - The list of keys to perform the bitwise operation on. + * + * Command Response - The size of the string stored in `destination`. + */ + public bitop( + operation: BitwiseOperation, + destination: string, + keys: string[], + ): T { + return this.addAndReturn(createBitOp(operation, destination, keys)); + } + /** * Returns the bit value at `offset` in the string value stored at `key`. `offset` must be greater than or equal * to zero. diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index a391dfa342..076d45cf49 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -13,6 +13,7 @@ import { import { gte } from "semver"; import { v4 as uuidv4 } from "uuid"; import { + BitwiseOperation, ClusterClientConfiguration, ClusterTransaction, GlideClusterClient, @@ -306,6 +307,7 @@ describe("GlideClusterClient", () => { client.blpop(["abc", "zxy", "lkn"], 0.1), client.rename("abc", "zxy"), client.brpop(["abc", "zxy", "lkn"], 0.1), + client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), client.smove("abc", "zxy", "value"), client.renamenx("abc", "zxy"), client.sinter(["abc", "zxy", "lkn"]), diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index a4bac4a8d1..cc215cd164 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -10,6 +10,7 @@ import { expect, it } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; import { + BitwiseOperation, ClosingError, ExpireOptions, GlideClient, @@ -473,6 +474,131 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bitop test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = `{key}-${uuidv4()}`; + const key2 = `{key}-${uuidv4()}`; + const keys = [key1, key2]; + const destination = `{key}-${uuidv4()}`; + const nonExistingKey1 = `{key}-${uuidv4()}`; + const nonExistingKey2 = `{key}-${uuidv4()}`; + const nonExistingKey3 = `{key}-${uuidv4()}`; + const nonExistingKeys = [ + nonExistingKey1, + nonExistingKey2, + nonExistingKey3, + ]; + const setKey = `{key}-${uuidv4()}`; + const value1 = "foobar"; + const value2 = "abcdef"; + + checkSimple(await client.set(key1, value1)).toEqual("OK"); + checkSimple(await client.set(key2, value2)).toEqual("OK"); + expect( + await client.bitop(BitwiseOperation.AND, destination, keys), + ).toEqual(6); + checkSimple(await client.get(destination)).toEqual("`bc`ab"); + expect( + await client.bitop(BitwiseOperation.OR, destination, keys), + ).toEqual(6); + checkSimple(await client.get(destination)).toEqual("goofev"); + + // reset values for simplicity of results in XOR + checkSimple(await client.set(key1, "a")).toEqual("OK"); + checkSimple(await client.set(key2, "b")).toEqual("OK"); + expect( + await client.bitop(BitwiseOperation.XOR, destination, keys), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("\u0003"); + + // test single source key + expect( + await client.bitop(BitwiseOperation.AND, destination, [ + key1, + ]), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("a"); + expect( + await client.bitop(BitwiseOperation.OR, destination, [ + key1, + ]), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("a"); + expect( + await client.bitop(BitwiseOperation.XOR, destination, [ + key1, + ]), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("a"); + expect( + await client.bitop(BitwiseOperation.NOT, destination, [ + key1, + ]), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("�"); + + expect(await client.setbit(key1, 0, 1)).toEqual(0); + expect( + await client.bitop(BitwiseOperation.NOT, destination, [ + key1, + ]), + ).toEqual(1); + checkSimple(await client.get(destination)).toEqual("\u001e"); + + // stores null when all keys hold empty strings + expect( + await client.bitop( + BitwiseOperation.AND, + destination, + nonExistingKeys, + ), + ).toEqual(0); + expect(await client.get(destination)).toBeNull(); + expect( + await client.bitop( + BitwiseOperation.OR, + destination, + nonExistingKeys, + ), + ).toEqual(0); + expect(await client.get(destination)).toBeNull(); + expect( + await client.bitop( + BitwiseOperation.XOR, + destination, + nonExistingKeys, + ), + ).toEqual(0); + expect(await client.get(destination)).toBeNull(); + expect( + await client.bitop(BitwiseOperation.NOT, destination, [ + nonExistingKey1, + ]), + ).toEqual(0); + expect(await client.get(destination)).toBeNull(); + + // invalid argument - source key list cannot be empty + await expect( + client.bitop(BitwiseOperation.OR, destination, []), + ).rejects.toThrow(RequestError); + + // invalid arguments - NOT cannot be passed more than 1 key + await expect( + client.bitop(BitwiseOperation.NOT, destination, keys), + ).rejects.toThrow(RequestError); + + expect(await client.sadd(setKey, ["foo"])).toEqual(1); + // invalid argument - source key has the wrong type + await expect( + client.bitop(BitwiseOperation.AND, destination, [setKey]), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `getbit test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 2003a99361..d35f203c26 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -10,6 +10,7 @@ import { gte } from "semver"; import { BaseClient, BaseClientConfiguration, + BitwiseOperation, ClusterTransaction, GeoUnit, GlideClient, @@ -361,6 +362,7 @@ export async function transactionTest( const key16 = "{key}" + uuidv4(); // list const key17 = "{key}" + uuidv4(); // bitmap const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET + const key19 = "{key}" + uuidv4(); // bitmap const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; @@ -649,6 +651,13 @@ export async function transactionTest( baseTransaction.bitcount(key17, new BitOffsetOptions(1, 1)); args.push(6); + baseTransaction.set(key19, "abcdef"); + args.push("OK"); + baseTransaction.bitop(BitwiseOperation.AND, key19, [key19, key17]); + args.push(6); + baseTransaction.get(key19); + args.push("`bc`ab"); + if (gte("7.0.0", version)) { baseTransaction.bitcount( key17,