From eee8855b78847efb7f7b8abb548dfaddec8326dc Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 19 Jul 2024 18:52:33 -0700 Subject: [PATCH] Node: Add `BITCOUNT` commnd. (#1982) * Add `BITCOUNT` commnd. Signed-off-by: Yury-Fridlyand Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 10 +++ node/src/BaseClient.ts | 28 ++++++++- node/src/Commands.ts | 13 ++++ node/src/Transaction.ts | 19 ++++++ node/src/commands/BitOffsetOptions.ts | 60 ++++++++++++++++++ node/tests/SharedTests.ts | 88 +++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 19 ++++++ 8 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 node/src/commands/BitOffsetOptions.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 10456b9734..2820b2c60a 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 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 SETBIT command ([#1978](https://github.com/valkey-io/valkey-glide/pull/1978)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index d3a2d6af84..513e6198f5 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -74,6 +74,11 @@ function loadNativeBinding() { function initialize() { const nativeBinding = loadNativeBinding(); const { + BitOffsetOptions, + BitmapIndexType, + ConditionalChange, + GeoAddOptions, + GeospatialData, GlideClient, GlideClusterClient, GlideClientConfiguration, @@ -121,6 +126,11 @@ function initialize() { } = nativeBinding; module.exports = { + BitOffsetOptions, + BitmapIndexType, + ConditionalChange, + GeoAddOptions, + GeospatialData, GlideClient, GlideClusterClient, GlideClientConfiguration, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 717525bc55..ad38a3a758 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -10,7 +10,6 @@ import { } from "glide-rs"; import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; -import { LPosOptions } from "./commands/LPosOptions"; import { ListDirection } from "./commands/ListDirection"; import { @@ -29,6 +28,7 @@ import { ZAddOptions, createBLPop, createBRPop, + createBitCount, createDecr, createDecrBy, createDel, @@ -127,6 +127,8 @@ import { createZRevRankWithScore, createZScore, } from "./Commands"; +import { BitOffsetOptions } from "./commands/BitOffsetOptions"; +import { LPosOptions } from "./commands/LPosOptions"; import { ClosingError, ConfigurationError, @@ -3381,6 +3383,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)); + } + /** * Adds geospatial members with their positions to the specified sorted set stored at `key`. * If a member is already a part of the sorted set, its position is updated. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 871a58fc63..7f7e9cf7bf 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -9,6 +9,7 @@ import { FlushMode } from "./commands/FlushMode"; import { ListDirection } from "./commands/ListDirection"; import { command_request } from "./ProtobufMessage"; +import { BitOffsetOptions } from "./commands/BitOffsetOptions"; import { GeospatialData } from "./commands/geospatial/GeospatialData"; import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; @@ -1594,6 +1595,18 @@ export function createFunctionLoad( return createCommand(RequestType.FunctionLoad, 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); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index e1f68b9d38..e0bbbd0d14 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -23,6 +23,7 @@ import { ZAddOptions, createBLPop, createBRPop, + createBitCount, createClientGetName, createClientId, createConfigGet, @@ -141,6 +142,7 @@ import { } from "./Commands"; import { command_request } from "./ProtobufMessage"; import { FlushMode } from "./commands/FlushMode"; +import { BitOffsetOptions } from "./commands/BitOffsetOptions"; import { LPosOptions } from "./commands/LPosOptions"; import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; import { GeospatialData } from "./commands/geospatial/GeospatialData"; @@ -1956,6 +1958,23 @@ export class BaseTransaction> { 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)); + } + /** * Adds geospatial members with their positions to the specified sorted set stored at `key`. * If a member is already a part of the sorted set, its position is updated. diff --git a/node/src/commands/BitOffsetOptions.ts b/node/src/commands/BitOffsetOptions.ts new file mode 100644 index 0000000000..64f6f8a82e --- /dev/null +++ b/node/src/commands/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 indexType?: 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.indexType = indexType; + } + + /** + * Converts BitOffsetOptions into a string[]. + * + * @returns string[] + */ + public toArgs(): string[] { + const args = [this.start.toString(), this.end.toString()]; + + if (this.indexType) args.push(this.indexType); + + return args; + } +} diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 76b429e853..af00fd424c 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -27,6 +27,10 @@ import { intoString, } from "./TestUtilities"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; +import { + BitmapIndexType, + BitOffsetOptions, +} from "../build-ts/src/commands/BitOffsetOptions"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; import { ListDirection } from "../build-ts/src/commands/ListDirection"; import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; @@ -4349,6 +4353,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, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `geoadd test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index d5be0ac2db..eff4652eda 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -18,11 +18,16 @@ import { ReturnType, Transaction, } from ".."; +import { + BitmapIndexType, + BitOffsetOptions, +} from "../build-ts/src/commands/BitOffsetOptions"; import { FlushMode } from "../build-ts/src/commands/FlushMode"; import { LPosOptions } from "../build-ts/src/commands/LPosOptions"; import { ListDirection } from "../build-ts/src/commands/ListDirection"; import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; import { checkIfServerVersionLessThan } from "./SharedTests"; +import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData"; beforeAll(() => { Logger.init("info"); @@ -633,6 +638,20 @@ export async function transactionTest( baseTransaction.setbit(key17, 1, 1); args.push(0); + 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);