diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f3d15f0a1..d39d8ac5ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002)) * Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991)) * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) +* Node: Added BITPOS command ([#1998](https://github.com/valkey-io/valkey-glide/pull/1998)) * 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)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 12df4b5b47..420529e0e7 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -74,8 +74,8 @@ function loadNativeBinding() { function initialize() { const nativeBinding = loadNativeBinding(); const { - BitOffsetOptions, BitmapIndexType, + BitOffsetOptions, BitwiseOperation, ConditionalChange, GeoAddOptions, @@ -130,8 +130,8 @@ function initialize() { } = nativeBinding; module.exports = { - BitOffsetOptions, BitmapIndexType, + BitOffsetOptions, BitwiseOperation, ConditionalChange, GeoAddOptions, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index b7d03103f6..0808339c44 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -12,6 +12,7 @@ import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { AggregationType, + BitmapIndexType, BitwiseOperation, ExpireOptions, GeoUnit, @@ -32,6 +33,7 @@ import { createBRPop, createBitCount, createBitOp, + createBitPos, createDecr, createDecrBy, createDel, @@ -1062,6 +1064,83 @@ export class BaseClient { return this.createWritePromise(createSetBit(key, offset, value)); } + /** + * Returns the position of the first bit matching the given `bit` value. The optional starting offset + * `start` is a zero-based index, with `0` being the first byte of the list, `1` being the next byte and so on. + * The offset can also be a negative number indicating an offset starting at the end of the list, with `-1` being + * the last byte of the list, `-2` being the penultimate, and so on. + * + * See https://valkey.io/commands/bitpos/ for more details. + * + * @param key - The key of the string. + * @param bit - The bit value to match. Must be `0` or `1`. + * @param start - (Optional) The starting offset. If not supplied, the search will start at the beginning of the string. + * @returns The position of the first occurrence of `bit` in the binary value of the string held at `key`. + * If `start` was provided, the search begins at the offset indicated by `start`. + * + * @example + * ```typescript + * await client.set("key1", "A1"); // "A1" has binary value 01000001 00110001 + * const result1 = await client.bitpos("key1", 1); + * console.log(result1); // Output: 1 - The first occurrence of bit value 1 in the string stored at "key1" is at the second position. + * + * const result2 = await client.bitpos("key1", 1, -1); + * console.log(result2); // Output: 10 - The first occurrence of bit value 1, starting at the last byte in the string stored at "key1", is at the eleventh position. + * ``` + */ + public async bitpos( + key: string, + bit: number, + start?: number, + ): Promise { + return this.createWritePromise(createBitPos(key, bit, start)); + } + + /** + * Returns the position of the first bit matching the given `bit` value. The offsets are zero-based indexes, with + * `0` being the first element of the list, `1` being the next, and so on. These offsets can also be negative + * numbers indicating offsets starting at the end of the list, with `-1` being the last element of the list, `-2` + * being the penultimate, and so on. + * + * If you are using Valkey 7.0.0 or above, the optional `indexType` can also be provided to specify whether the + * `start` and `end` offsets specify BIT or BYTE offsets. If `indexType` is not provided, BYTE offsets + * are assumed. If BIT is specified, `start=0` and `end=2` means to look at the first three bits. If BYTE is + * specified, `start=0` and `end=2` means to look at the first three bytes. + * + * See https://valkey.io/commands/bitpos/ for more details. + * + * @param key - The key of the string. + * @param bit - The bit value to match. Must be `0` or `1`. + * @param start - The starting offset. + * @param end - The ending offset. + * @param indexType - (Optional) The index offset type. This option can only be specified if you are using Valkey + * 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. + * @returns The position of the first occurrence from the `start` to the `end` offsets of the `bit` in the binary + * value of the string held at `key`. + * + * @example + * ```typescript + * await client.set("key1", "A12"); // "A12" has binary value 01000001 00110001 00110010 + * const result1 = await client.bitposInterval("key1", 1, 1, -1); + * console.log(result1); // Output: 10 - The first occurrence of bit value 1 in the second byte to the last byte of the string stored at "key1" is at the eleventh position. + * + * const result2 = await client.bitposInterval("key1", 1, 2, 9, BitmapIndexType.BIT); + * console.log(result2); // Output: 7 - The first occurrence of bit value 1 in the third to tenth bits of the string stored at "key1" is at the eighth position. + * ``` + */ + public async bitposInterval( + key: string, + bit: number, + start: number, + end: number, + indexType?: BitmapIndexType, + ): Promise { + return this.createWritePromise( + createBitPos(key, bit, start, end, indexType), + ); + } + /** Retrieve the value associated with `field` in the hash stored at `key`. * See https://valkey.io/commands/hget/ for details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 7b20ae39f4..0a85954fde 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1706,6 +1706,47 @@ export function createBitCount( return createCommand(RequestType.BitCount, args); } +/** + * 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. + * Can also be specified as an optional argument to the {@link BaseClient.bitposInverval|bitposInterval} command. + * + * since - Valkey version 7.0.0. + */ +export enum BitmapIndexType { + /** Specifies that provided indexes are byte indexes. */ + BYTE = "BYTE", + /** Specifies that provided indexes are bit indexes. */ + BIT = "BIT", +} + +/** + * @internal + */ +export function createBitPos( + key: string, + bit: number, + start?: number, + end?: number, + indexType?: BitmapIndexType, +): command_request.Command { + const args = [key, bit.toString()]; + + if (start !== undefined) { + args.push(start.toString()); + } + + if (end !== undefined) { + args.push(end.toString()); + } + + if (indexType) { + args.push(indexType); + } + + return createCommand(RequestType.BitPos, 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 801b093955..8521779848 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -4,6 +4,7 @@ import { AggregationType, + BitmapIndexType, BitwiseOperation, ExpireOptions, GeoUnit, @@ -26,6 +27,7 @@ import { createBRPop, createBitCount, createBitOp, + createBitPos, createClientGetName, createClientId, createConfigGet, @@ -455,6 +457,59 @@ export class BaseTransaction> { return this.addAndReturn(createSetBit(key, offset, value)); } + /** + * Returns the position of the first bit matching the given `bit` value. The optional starting offset + * `start` is a zero-based index, with `0` being the first byte of the list, `1` being the next byte and so on. + * The offset can also be a negative number indicating an offset starting at the end of the list, with `-1` being + * the last byte of the list, `-2` being the penultimate, and so on. + * + * See https://valkey.io/commands/bitpos/ for more details. + * + * @param key - The key of the string. + * @param bit - The bit value to match. Must be `0` or `1`. + * @param start - (Optional) The starting offset. If not supplied, the search will start at the beginning of the string. + * + * Command Response - The position of the first occurrence of `bit` in the binary value of the string held at `key`. + * If `start` was provided, the search begins at the offset indicated by `start`. + */ + public bitpos(key: string, bit: number, start?: number): T { + return this.addAndReturn(createBitPos(key, bit, start)); + } + + /** + * Returns the position of the first bit matching the given `bit` value. The offsets are zero-based indexes, with + * `0` being the first element of the list, `1` being the next, and so on. These offsets can also be negative + * numbers indicating offsets starting at the end of the list, with `-1` being the last element of the list, `-2` + * being the penultimate, and so on. + * + * If you are using Valkey 7.0.0 or above, the optional `indexType` can also be provided to specify whether the + * `start` and `end` offsets specify BIT or BYTE offsets. If `indexType` is not provided, BYTE offsets + * are assumed. If BIT is specified, `start=0` and `end=2` means to look at the first three bits. If BYTE is + * specified, `start=0` and `end=2` means to look at the first three bytes. + * + * See https://valkey.io/commands/bitpos/ for more details. + * + * @param key - The key of the string. + * @param bit - The bit value to match. Must be `0` or `1`. + * @param start - The starting offset. + * @param end - The ending offset. + * @param indexType - (Optional) The index offset type. This option can only be specified if you are using Valkey + * 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. + * + * Command Response - The position of the first occurrence from the `start` to the `end` offsets of the `bit` in the + * binary value of the string held at `key`. + */ + public bitposInterval( + key: string, + bit: number, + start: number, + end: number, + indexType?: BitmapIndexType, + ): T { + return this.addAndReturn(createBitPos(key, bit, start, end, indexType)); + } + /** Reads the configuration parameters of a running Redis server. * See https://valkey.io/commands/config-get/ for details. * diff --git a/node/src/commands/BitOffsetOptions.ts b/node/src/commands/BitOffsetOptions.ts index 64f6f8a82e..5f5d6800e6 100644 --- a/node/src/commands/BitOffsetOptions.ts +++ b/node/src/commands/BitOffsetOptions.ts @@ -5,19 +5,7 @@ // 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", -} +import { BitmapIndexType } from "src/Commands"; /** * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 5b74140add..564ce1bd6e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -10,6 +10,7 @@ import { expect, it } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; import { + BitmapIndexType, BitwiseOperation, ClosingError, ExpireOptions, @@ -35,10 +36,7 @@ import { intoString, } from "./TestUtilities"; import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient"; -import { - BitmapIndexType, - BitOffsetOptions, -} from "../build-ts/src/commands/BitOffsetOptions"; +import { 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"; @@ -661,6 +659,131 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `bitpos and bitposInterval test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + const key = `{key}-${uuidv4()}`; + const nonExistingKey = `{key}-${uuidv4()}`; + const setKey = `{key}-${uuidv4()}`; + const value = "?f0obar"; // 00111111 01100110 00110000 01101111 01100010 01100001 01110010 + + checkSimple(await client.set(key, value)).toEqual("OK"); + expect(await client.bitpos(key, 0)).toEqual(0); + expect(await client.bitpos(key, 1)).toEqual(2); + expect(await client.bitpos(key, 1, 1)).toEqual(9); + expect(await client.bitposInterval(key, 0, 3, 5)).toEqual(24); + + // -1 is returned if start > end + expect(await client.bitposInterval(key, 0, 1, 0)).toEqual(-1); + + // `BITPOS` returns -1 for non-existing strings + expect(await client.bitpos(nonExistingKey, 1)).toEqual(-1); + expect( + await client.bitposInterval(nonExistingKey, 1, 3, 5), + ).toEqual(-1); + + // invalid argument - bit value must be 0 or 1 + await expect(client.bitpos(key, 2)).rejects.toThrow( + RequestError, + ); + await expect( + client.bitposInterval(key, 2, 3, 5), + ).rejects.toThrow(RequestError); + + // key exists, but it is not a string + expect(await client.sadd(setKey, ["foo"])).toEqual(1); + await expect(client.bitpos(setKey, 1)).rejects.toThrow( + RequestError, + ); + await expect( + client.bitposInterval(setKey, 1, 1, -1), + ).rejects.toThrow(RequestError); + + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + await expect( + client.bitposInterval( + key, + 1, + 1, + -1, + BitmapIndexType.BYTE, + ), + ).rejects.toThrow(RequestError); + await expect( + client.bitposInterval( + key, + 1, + 1, + -1, + BitmapIndexType.BIT, + ), + ).rejects.toThrow(RequestError); + } else { + expect( + await client.bitposInterval( + key, + 0, + 3, + 5, + BitmapIndexType.BYTE, + ), + ).toEqual(24); + expect( + await client.bitposInterval( + key, + 1, + 43, + -2, + BitmapIndexType.BIT, + ), + ).toEqual(47); + expect( + await client.bitposInterval( + nonExistingKey, + 1, + 3, + 5, + BitmapIndexType.BYTE, + ), + ).toEqual(-1); + expect( + await client.bitposInterval( + nonExistingKey, + 1, + 3, + 5, + BitmapIndexType.BIT, + ), + ).toEqual(-1); + + // -1 is returned if the bit value wasn't found + expect( + await client.bitposInterval( + key, + 1, + -1, + -1, + BitmapIndexType.BIT, + ), + ).toEqual(-1); + + // key exists, but it is not a string + await expect( + client.bitposInterval( + setKey, + 1, + 1, + -1, + BitmapIndexType.BIT, + ), + ).rejects.toThrow(RequestError); + } + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `config get and config set with timeout parameter_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 352f9f3dab..d158350556 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -10,6 +10,7 @@ import { gte } from "semver"; import { BaseClient, BaseClientConfiguration, + BitmapIndexType, BitwiseOperation, ClusterTransaction, GeoUnit, @@ -23,10 +24,7 @@ import { ScoreFilter, Transaction, } from ".."; -import { - BitmapIndexType, - BitOffsetOptions, -} from "../build-ts/src/commands/BitOffsetOptions"; +import { BitOffsetOptions } from "../build-ts/src/commands/BitOffsetOptions"; 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"; @@ -745,6 +743,8 @@ export async function transactionTest( responseData.push(["bitcount(key17)", 26]); baseTransaction.bitcount(key17, new BitOffsetOptions(1, 1)); responseData.push(["bitcount(key17, new BitOffsetOptions(1, 1))", 6]); + baseTransaction.bitpos(key17, 1); + responseData.push(["bitpos(key17, 1)", 1]); baseTransaction.set(key19, "abcdef"); responseData.push(['set(key19, "abcdef")', "OK"]); @@ -765,6 +765,11 @@ export async function transactionTest( "bitcount(key17, new BitOffsetOptions(5, 30, BitmapIndexType.BIT))", 17, ]); + baseTransaction.bitposInterval(key17, 1, 44, 50, BitmapIndexType.BIT); + responseData.push([ + "bitposInterval(key17, 1, 44, 50, BitmapIndexType.BIT)", + 46, + ]); } baseTransaction.pfadd(key11, ["a", "b", "c"]);