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 BITCOUNT commnd. #1982

Merged
merged 6 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
@@ -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 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
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 @@ -120,6 +125,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 {
AggregationType,
ExpireOptions,
Expand All @@ -27,6 +26,7 @@ import {
ZAddOptions,
createBLPop,
createBRPop,
createBitCount,
createDecr,
createDecrBy,
createDel,
Expand Down Expand Up @@ -123,6 +123,8 @@ import {
createZRevRankWithScore,
createZScore,
} from "./Commands";
import { BitOffsetOptions } from "./commands/BitOffsetOptions";
import { LPosOptions } from "./commands/LPosOptions";
import {
ClosingError,
ConfigurationError,
Expand Down Expand Up @@ -3314,6 +3316,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 @@ -7,6 +7,7 @@ import Long from "long";
import { LPosOptions } from "./commands/LPosOptions";

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 @@ -1565,6 +1566,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);
}

export type StreamReadOptions = {
/**
* If set, the read request will block for the set amount of milliseconds or
Expand Down
21 changes: 20 additions & 1 deletion node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/

import { BitOffsetOptions } from "./commands/BitOffsetOptions";
import { LPosOptions } from "./commands/LPosOptions";
import {
AggregationType,
Expand All @@ -22,6 +23,7 @@ import {
ZAddOptions,
createBLPop,
createBRPop,
createBitCount,
createClientGetName,
createClientId,
createConfigGet,
Expand Down Expand Up @@ -94,8 +96,8 @@ import {
createSInterCard,
createSInterStore,
createSIsMember,
createSMembers,
createSMIsMember,
createSMembers,
createSMove,
createSPop,
createSRem,
Expand Down Expand Up @@ -1877,6 +1879,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 @@ -28,6 +28,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 { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData";
import { GeoAddOptions } from "../build-ts/src/commands/geospatial/GeoAddOptions";
Expand Down Expand Up @@ -4268,6 +4272,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
20 changes: 19 additions & 1 deletion node/tests/TestUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import {
ReturnType,
Transaction,
} from "..";
import { checkIfServerVersionLessThan } from "./SharedTests";
import {
BitmapIndexType,
BitOffsetOptions,
} from "../build-ts/src/commands/BitOffsetOptions";
import { LPosOptions } from "../build-ts/src/commands/LPosOptions";
import { checkIfServerVersionLessThan } from "./SharedTests";
import { GeospatialData } from "../build-ts/src/commands/geospatial/GeospatialData";

beforeAll(() => {
Expand Down Expand Up @@ -620,6 +624,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
Loading