Skip to content

Commit

Permalink
Node: add BITOP command (#2012)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaron-congo authored Jul 24, 2024
1 parent 9a69ac1 commit b9824c2
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions node/npm/glide/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function initialize() {
const {
BitOffsetOptions,
BitmapIndexType,
BitwiseOperation,
ConditionalChange,
GeoAddOptions,
GeospatialData,
Expand Down Expand Up @@ -130,6 +131,7 @@ function initialize() {
module.exports = {
BitOffsetOptions,
BitmapIndexType,
BitwiseOperation,
ConditionalChange,
GeoAddOptions,
GeospatialData,
Expand Down
35 changes: 35 additions & 0 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as net from "net";
import { Buffer, BufferWriter, Reader, Writer } from "protobufjs";
import {
AggregationType,
BitwiseOperation,
ExpireOptions,
InsertPosition,
KeyWeight,
Expand All @@ -28,6 +29,7 @@ import {
createBLPop,
createBRPop,
createBitCount,
createBitOp,
createDecr,
createDecrBy,
createDel,
Expand Down Expand Up @@ -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<number> {
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.
Expand Down
22 changes: 22 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
22 changes: 22 additions & 0 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import {
AggregationType,
BitwiseOperation,
ExpireOptions,
InfoOptions,
InsertPosition,
Expand All @@ -22,6 +23,7 @@ import {
createBLPop,
createBRPop,
createBitCount,
createBitOp,
createClientGetName,
createClientId,
createConfigGet,
Expand Down Expand Up @@ -395,6 +397,26 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
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.
Expand Down
2 changes: 2 additions & 0 deletions node/tests/RedisClusterClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { gte } from "semver";
import { v4 as uuidv4 } from "uuid";
import {
BitwiseOperation,
ClusterClientConfiguration,
ClusterTransaction,
GlideClusterClient,
Expand Down Expand Up @@ -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"]),
Expand Down
126 changes: 126 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { expect, it } from "@jest/globals";
import { v4 as uuidv4 } from "uuid";
import {
BitwiseOperation,
ClosingError,
ExpireOptions,
GlideClient,
Expand Down Expand Up @@ -473,6 +474,131 @@ export function runBaseTests<Context>(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) => {
Expand Down
9 changes: 9 additions & 0 deletions node/tests/TestUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { gte } from "semver";
import {
BaseClient,
BaseClientConfiguration,
BitwiseOperation,
ClusterTransaction,
GeoUnit,
GlideClient,
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit b9824c2

Please sign in to comment.