From e71f6fe58c621f09f5ed9d68f41efbe188d36b4c Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Thu, 18 Jul 2024 18:54:44 -0700 Subject: [PATCH 1/2] Add `BITCOUNT` commnd. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 26 ++++++ node/src/Commands.ts | 13 +++ node/src/Transaction.ts | 21 ++++- node/src/command-options/BitOffsetOptions.ts | 60 +++++++++++++ node/tests/SharedTests.ts | 88 ++++++++++++++++++++ node/tests/TestUtilities.ts | 21 +++++ 7 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 node/src/command-options/BitOffsetOptions.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 911d93abc5..b05f4b7ad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) * Node: Added GETDEL command ([#1968](https://github.com/valkey-io/valkey-glide/pull/1968)) * Node: Added LPUSHX and RPUSHX command([#1959](https://github.com/valkey-io/valkey-glide/pull/1959)) * Node: Added LSET command ([#1952](https://github.com/valkey-io/valkey-glide/pull/1952)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 99ae4af26a..248b1cba40 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -10,6 +10,7 @@ import { } from "glide-rs"; import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; +import { BitOffsetOptions } from "./command-options/BitOffsetOptions"; import { LPosOptions } from "./command-options/LPosOptions"; import { AggregationType, @@ -27,6 +28,7 @@ import { ZAddOptions, createBLPop, createBRPop, + createBitCount, createDecr, createDecrBy, createDel, @@ -3206,6 +3208,30 @@ export class BaseClient { return this.createWritePromise(createLPos(key, element, options)); } + /** + * Counts the number of set bits (population counting) in the string stored at `key`. The `options` argument can + * optionally be provided to count the number of bits in a specific string interval. + * + * See https://valkey.io/commands/bitcount for more details. + * + * @param key - The key for the string to count the set bits of. + * @param options - The offset options. + * @returns If `options` is provided, returns the number of set bits in the string interval specified by `options`. + * If `options` is not provided, returns the number of set bits in the string stored at `key`. + * Otherwise, if `key` is missing, returns `0` as it is treated as an empty string. + * + * @example + * ```typescript + * console.log(await client.bitcount("my_key1")); // Output: 2 - The string stored at "my_key1" contains 2 set bits. + * console.log(await client.bitcount("my_key2", OffsetOptions(1, 3))); // Output: 2 - The second to fourth bytes of the string stored at "my_key2" contain 2 set bits. + * console.log(await client.bitcount("my_key3", OffsetOptions(1, 1, BitmapIndexType.BIT))); // Output: 1 - Indicates that the second bit of the string stored at "my_key3" is set. + * console.log(await client.bitcount("my_key3", OffsetOptions(-1, -1, BitmapIndexType.BIT))); // Output: 1 - Indicates that the last bit of the string stored at "my_key3" is set. + * ``` + */ + public bitcount(key: string, options?: BitOffsetOptions): Promise { + return this.createWritePromise(createBitCount(key, options)); + } + /** * @internal */ diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 9fbf4ccf57..871e765eed 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -7,6 +7,7 @@ import Long from "long"; import { LPosOptions } from "./command-options/LPosOptions"; import { command_request } from "./ProtobufMessage"; +import { BitOffsetOptions } from "./command-options/BitOffsetOptions"; import RequestType = command_request.RequestType; @@ -1526,6 +1527,18 @@ export function createBLPop( return createCommand(RequestType.BLPop, args); } +/** + * @internal + */ +export function createBitCount( + key: string, + options?: BitOffsetOptions, +): command_request.Command { + const args = [key]; + if (options) args.push(...options.toArgs()); + return createCommand(RequestType.BitCount, args); +} + export type StreamReadOptions = { /** * If set, the read request will block for the set amount of milliseconds or diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 767b96cf55..49a4c5175d 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -2,6 +2,7 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +import { BitOffsetOptions } from "./command-options/BitOffsetOptions"; import { LPosOptions } from "./command-options/LPosOptions"; import { AggregationType, @@ -22,6 +23,7 @@ import { ZAddOptions, createBLPop, createBRPop, + createBitCount, createClientGetName, createClientId, createConfigGet, @@ -93,8 +95,8 @@ import { createSInterCard, createSInterStore, createSIsMember, - createSMembers, createSMIsMember, + createSMembers, createSMove, createSPop, createSRem, @@ -1779,6 +1781,23 @@ export class BaseTransaction> { public dbsize(): T { return this.addAndReturn(createDBSize()); } + + /** + * Counts the number of set bits (population counting) in the string stored at `key`. The `options` argument can + * optionally be provided to count the number of bits in a specific string interval. + * + * See https://valkey.io/commands/bitcount for more details. + * + * @param key - The key for the string to count the set bits of. + * @param options - The offset options. + * + * Command Response - If `options` is provided, returns the number of set bits in the string interval specified by `options`. + * If `options` is not provided, returns the number of set bits in the string stored at `key`. + * Otherwise, if `key` is missing, returns `0` as it is treated as an empty string. + */ + public bitcount(key: string, options?: BitOffsetOptions): T { + return this.addAndReturn(createBitCount(key, options)); + } } /** diff --git a/node/src/command-options/BitOffsetOptions.ts b/node/src/command-options/BitOffsetOptions.ts new file mode 100644 index 0000000000..89831e7114 --- /dev/null +++ b/node/src/command-options/BitOffsetOptions.ts @@ -0,0 +1,60 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +// Import below added to fix up the TSdoc link, but eslint blames for unused import. +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +import { BaseClient } from "src/BaseClient"; + +/** + * Enumeration specifying if index arguments are BYTE indexes or BIT indexes. + * Can be specified in {@link BitOffsetOptions}, which is an optional argument to the {@link BaseClient.bitcount|bitcount} command. + * + * since - Valkey version 7.0.0. + */ +export enum BitmapIndexType { + /** Specifies that indexes provided to {@link BitOffsetOptions} are byte indexes. */ + BYTE = "BYTE", + /** Specifies that indexes provided to {@link BitOffsetOptions} are bit indexes. */ + BIT = "BIT", +} + +/** + * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are + * zero-based indexes, with `0` being the first index of the string, `1` being the next index and so on. + * The offsets can also be negative numbers indicating offsets starting at the end of the string, with `-1` being + * the last index of the string, `-2` being the penultimate, and so on. + * + * See https://valkey.io/commands/bitcount/ for more details. + */ +export class BitOffsetOptions { + private start: number; + private end: number; + private intexType?: BitmapIndexType; + + /** + * @param start - The starting offset index. + * @param end - The ending offset index. + * @param indexType - The index offset type. This option can only be specified if you are using server version 7.0.0 or above. + * Could be either {@link BitmapIndexType.BYTE} or {@link BitmapIndexType.BIT}. + * If no index type is provided, the indexes will be assumed to be byte indexes. + */ + constructor(start: number, end: number, indexType?: BitmapIndexType) { + this.start = start; + this.end = end; + this.intexType = indexType; + } + + /** + * Converts BitOffsetOptions into a string[]. + * + * @returns string[] + */ + public toArgs(): string[] { + const args = [this.start.toString(), this.end.toString()]; + + if (this.intexType) args.push(this.intexType); + + return args; + } +} diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index c38534a88f..b59d32d81f 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -29,6 +29,10 @@ import { } from "./TestUtilities"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; import { LPosOptions } from "../build-ts/src/command-options/LPosOptions"; +import { + BitmapIndexType, + BitOffsetOptions, +} from "../build-ts/src/command-options/BitOffsetOptions"; async function getVersion(): Promise<[number, number, number]> { const versionString = await new Promise((resolve, reject) => { @@ -4110,6 +4114,90 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bitcount test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const key2 = uuidv4(); + const value = "foobar"; + + checkSimple(await client.set(key1, value)).toEqual("OK"); + expect(await client.bitcount(key1)).toEqual(26); + expect( + await client.bitcount(key1, new BitOffsetOptions(1, 1)), + ).toEqual(6); + expect( + await client.bitcount(key1, new BitOffsetOptions(0, -5)), + ).toEqual(10); + // non-existing key + expect(await client.bitcount(uuidv4())).toEqual(0); + expect( + await client.bitcount( + uuidv4(), + new BitOffsetOptions(5, 30), + ), + ).toEqual(0); + // key exists, but it is not a string + expect(await client.sadd(key2, [value])).toEqual(1); + await expect(client.bitcount(key2)).rejects.toThrow( + RequestError, + ); + await expect( + client.bitcount(key2, new BitOffsetOptions(1, 1)), + ).rejects.toThrow(RequestError); + + if (await checkIfServerVersionLessThan("7.0.0")) { + await expect( + client.bitcount( + key1, + new BitOffsetOptions(2, 5, BitmapIndexType.BIT), + ), + ).rejects.toThrow(); + await expect( + client.bitcount( + key1, + new BitOffsetOptions(2, 5, BitmapIndexType.BYTE), + ), + ).rejects.toThrow(); + } else { + expect( + await client.bitcount( + key1, + new BitOffsetOptions(2, 5, BitmapIndexType.BYTE), + ), + ).toEqual(16); + expect( + await client.bitcount( + key1, + new BitOffsetOptions(5, 30, BitmapIndexType.BIT), + ), + ).toEqual(17); + expect( + await client.bitcount( + key1, + new BitOffsetOptions(5, -5, BitmapIndexType.BIT), + ), + ).toEqual(23); + expect( + await client.bitcount( + uuidv4(), + new BitOffsetOptions(2, 5, BitmapIndexType.BYTE), + ), + ).toEqual(0); + // key exists, but it is not a string + await expect( + client.bitcount( + key2, + new BitOffsetOptions(1, 1, BitmapIndexType.BYTE), + ), + ).rejects.toThrow(RequestError); + } + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 43d1df5fe3..509d7c8bfb 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -20,6 +20,10 @@ import { } from ".."; import { checkIfServerVersionLessThan } from "./SharedTests"; import { LPosOptions } from "../build-ts/src/command-options/LPosOptions"; +import { + BitmapIndexType, + BitOffsetOptions, +} from "../build-ts/src/command-options/BitOffsetOptions"; beforeAll(() => { Logger.init("info"); @@ -312,6 +316,7 @@ export async function transactionTest( const key14 = "{key}" + uuidv4(); // sorted set const key15 = "{key}" + uuidv4(); // list const key16 = "{key}" + uuidv4(); // list + const key17 = "{key}" + uuidv4(); // bitmap const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; @@ -566,6 +571,22 @@ export async function transactionTest( args.push([key6, field + "3"]); baseTransaction.blpop([key6], 0.1); args.push([key6, field + "1"]); + + baseTransaction.set(key17, "foobar"); + args.push("OK"); + baseTransaction.bitcount(key17); + args.push(26); + baseTransaction.bitcount(key17, new BitOffsetOptions(1, 1)); + args.push(6); + + if (!(await checkIfServerVersionLessThan("7.0.0"))) { + baseTransaction.bitcount( + key17, + new BitOffsetOptions(5, 30, BitmapIndexType.BIT), + ); + args.push(17); + } + baseTransaction.pfadd(key11, ["a", "b", "c"]); args.push(1); baseTransaction.pfcount([key11]); From 171f165a3084068a330604cfbe0d1b674a40f119 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 19 Jul 2024 10:47:31 -0700 Subject: [PATCH 2/2] Signed-off-by: Yury-Fridlyand --- node/src/command-options/BitOffsetOptions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node/src/command-options/BitOffsetOptions.ts b/node/src/command-options/BitOffsetOptions.ts index 89831e7114..64f6f8a82e 100644 --- a/node/src/command-options/BitOffsetOptions.ts +++ b/node/src/command-options/BitOffsetOptions.ts @@ -30,7 +30,7 @@ export enum BitmapIndexType { export class BitOffsetOptions { private start: number; private end: number; - private intexType?: BitmapIndexType; + private indexType?: BitmapIndexType; /** * @param start - The starting offset index. @@ -42,7 +42,7 @@ export class BitOffsetOptions { constructor(start: number, end: number, indexType?: BitmapIndexType) { this.start = start; this.end = end; - this.intexType = indexType; + this.indexType = indexType; } /** @@ -53,7 +53,7 @@ export class BitOffsetOptions { public toArgs(): string[] { const args = [this.start.toString(), this.end.toString()]; - if (this.intexType) args.push(this.intexType); + if (this.indexType) args.push(this.indexType); return args; }