Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node: add BITOP command #2012

Merged
merged 2 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -28,6 +28,7 @@ import {
createBLPop,
createBRPop,
createBitCount,
createBitOp,
createDecr,
createDecrBy,
createDel,
Expand Down Expand Up @@ -132,6 +133,7 @@ import {
GeoUnit,
} from "./Commands";
import { BitOffsetOptions } from "./commands/BitOffsetOptions";
import { BitwiseOperation } from "./commands/BitwiseOperation";
import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions";
import { GeospatialData } from "./commands/geospatial/GeospatialData";
import { LPosOptions } from "./commands/LPosOptions";
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
12 changes: 12 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { LPosOptions } from "./commands/LPosOptions";

import { command_request } from "./ProtobufMessage";
import { BitOffsetOptions } from "./commands/BitOffsetOptions";
import { BitwiseOperation } from "./commands/BitwiseOperation";
import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions";
import { GeospatialData } from "./commands/geospatial/GeospatialData";

Expand Down Expand Up @@ -446,6 +447,17 @@ export function createDecrBy(
return createCommand(RequestType.DecrBy, [key, amount.toString()]);
}

/**
* @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 @@ -22,6 +22,7 @@ import {
createBLPop,
createBRPop,
createBitCount,
createBitOp,
createClientGetName,
createClientId,
createConfigGet,
Expand Down Expand Up @@ -146,6 +147,7 @@ import {
} from "./Commands";
import { command_request } from "./ProtobufMessage";
import { BitOffsetOptions } from "./commands/BitOffsetOptions";
import { BitwiseOperation } from "./commands/BitwiseOperation";
import { FlushMode } from "./commands/FlushMode";
import { LPosOptions } from "./commands/LPosOptions";
import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions";
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
18 changes: 18 additions & 0 deletions node/src/commands/BitwiseOperation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/

// Import below added to fix up the TSdoc link, but eslint complains about unused import.
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
import { BaseClient } from "src/BaseClient";

/**
* 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 {
aaron-congo marked this conversation as resolved.
Show resolved Hide resolved
AND = "AND",
OR = "OR",
XOR = "XOR",
NOT = "NOT",
}
2 changes: 2 additions & 0 deletions node/tests/RedisClusterClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
parseEndpoints,
transactionTest,
} from "./TestUtilities";
import { BitwiseOperation } from "../src/commands/BitwiseOperation";
type Context = {
client: 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 @@ -37,6 +37,7 @@ import {
BitmapIndexType,
BitOffsetOptions,
} from "../build-ts/src/commands/BitOffsetOptions";
import { BitwiseOperation } from "../src/commands/BitwiseOperation";
import { LPosOptions } from "../build-ts/src/commands/LPosOptions";
import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData";
import { GeoAddOptions } from "../build-ts/src/commands/geospatial/GeoAddOptions";
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 @@ -25,6 +25,7 @@ import {
BitmapIndexType,
BitOffsetOptions,
} from "../build-ts/src/commands/BitOffsetOptions";
import { BitwiseOperation } from "../build-ts/src/commands/BitwiseOperation";
import { FlushMode } from "../build-ts/src/commands/FlushMode";
import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData";
import { LPosOptions } from "../build-ts/src/commands/LPosOptions";
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
Loading