Skip to content

Commit

Permalink
Node: Add BITCOUNT commnd. (valkey-io#1982)
Browse files Browse the repository at this point in the history
* Add `BITCOUNT` commnd.

Signed-off-by: Yury-Fridlyand <[email protected]>
Signed-off-by: Chloe Yip <[email protected]>
  • Loading branch information
Yury-Fridlyand authored and cyip10 committed Jul 23, 2024
1 parent 8cdb26f commit eee8855
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#### Changes
* Node: Added BITCOUNT command ([#1982](https:/valkey-io/valkey-glide/pull/1982))
* Node: Added FLUSHDB command ([#1986](https:/valkey-io/valkey-glide/pull/1986))
* Node: Added GETDEL command ([#1968](https:/valkey-io/valkey-glide/pull/1968))
* Node: Added SETBIT command ([#1978](https:/valkey-io/valkey-glide/pull/1978))
Expand Down
10 changes: 10 additions & 0 deletions node/npm/glide/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ function loadNativeBinding() {
function initialize() {
const nativeBinding = loadNativeBinding();
const {
BitOffsetOptions,
BitmapIndexType,
ConditionalChange,
GeoAddOptions,
GeospatialData,
GlideClient,
GlideClusterClient,
GlideClientConfiguration,
Expand Down Expand Up @@ -121,6 +126,11 @@ function initialize() {
} = nativeBinding;

module.exports = {
BitOffsetOptions,
BitmapIndexType,
ConditionalChange,
GeoAddOptions,
GeospatialData,
GlideClient,
GlideClusterClient,
GlideClientConfiguration,
Expand Down
28 changes: 27 additions & 1 deletion node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -29,6 +28,7 @@ import {
ZAddOptions,
createBLPop,
createBRPop,
createBitCount,
createDecr,
createDecrBy,
createDel,
Expand Down Expand Up @@ -127,6 +127,8 @@ import {
createZRevRankWithScore,
createZScore,
} from "./Commands";
import { BitOffsetOptions } from "./commands/BitOffsetOptions";
import { LPosOptions } from "./commands/LPosOptions";
import {
ClosingError,
ConfigurationError,
Expand Down Expand Up @@ -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<number> {
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.
Expand Down
13 changes: 13 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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
*/
Expand Down
19 changes: 19 additions & 0 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ZAddOptions,
createBLPop,
createBRPop,
createBitCount,
createClientGetName,
createClientId,
createConfigGet,
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -1956,6 +1958,23 @@ export class BaseTransaction<T extends BaseTransaction<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));
}

/**
* 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.
Expand Down
60 changes: 60 additions & 0 deletions node/src/commands/BitOffsetOptions.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
88 changes: 88 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -4349,6 +4353,90 @@ export function runBaseTests<Context>(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) => {
Expand Down
19 changes: 19 additions & 0 deletions node/tests/TestUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit eee8855

Please sign in to comment.