From a0c8f07994eef12d7f148bdf3aeec71fdb7a21da Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 31 Jul 2024 17:53:22 -0700 Subject: [PATCH] Node: Add `SORT` command. (#2028) * Add `SORT` command. Signed-off-by: Yury-Fridlyand Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 4 + node/src/Commands.ts | 124 ++++++++++++++ node/src/GlideClient.ts | 99 +++++++++++ node/src/GlideClusterClient.ts | 93 +++++++++++ node/src/Transaction.ts | 139 ++++++++++++++++ node/tests/RedisClient.test.ts | 228 +++++++++++++++++++++++++- node/tests/RedisClusterClient.test.ts | 89 +++++++++- node/tests/TestUtilities.ts | 37 ++++- 9 files changed, 803 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 975866273a..2d9f057baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added SORT commands ([#2028](https://github.com/valkey-io/valkey-glide/pull/2028)) * Node: Added LASTSAVE command ([#2059](https://github.com/valkey-io/valkey-glide/pull/2059)) * Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049)) * Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index b64b44aa86..f881d7c054 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -130,6 +130,8 @@ function initialize() { RangeByLex, ReadFrom, RedisCredentials, + SortClusterOptions, + SortOptions, SortedSetRange, StreamTrimOptions, StreamAddOptions, @@ -214,6 +216,8 @@ function initialize() { RangeByLex, ReadFrom, RedisCredentials, + SortClusterOptions, + SortOptions, SortedSetRange, StreamTrimOptions, StreamAddOptions, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index f23b972459..105d8bbca4 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2804,6 +2804,130 @@ export function createZIncrBy( ]); } +/** + * Optional arguments to {@link GlideClient.sort|sort}, {@link GlideClient.sortStore|sortStore} and {@link GlideClient.sortReadOnly|sortReadOnly} commands. + * + * See https://valkey.io/commands/sort/ for more details. + */ +export type SortOptions = SortBaseOptions & { + /** + * A pattern to sort by external keys instead of by the elements stored at the key themselves. The + * pattern should contain an asterisk (*) as a placeholder for the element values, where the value + * from the key replaces the asterisk to create the key name. For example, if `key` + * contains IDs of objects, `byPattern` can be used to sort these IDs based on an + * attribute of the objects, like their weights or timestamps. + */ + byPattern?: string; + + /** + * A pattern used to retrieve external keys' values, instead of the elements at `key`. + * The pattern should contain an asterisk (`*`) as a placeholder for the element values, where the + * value from `key` replaces the asterisk to create the `key` name. This + * allows the sorted elements to be transformed based on the related keys values. For example, if + * `key` contains IDs of users, `getPatterns` can be used to retrieve + * specific attributes of these users, such as their names or email addresses. E.g., if + * `getPatterns` is `name_*`, the command will return the values of the keys + * `name_` for each sorted element. Multiple `getPatterns` + * arguments can be provided to retrieve multiple attributes. The special value `#` can + * be used to include the actual element from `key` being sorted. If not provided, only + * the sorted elements themselves are returned. + */ + getPatterns?: string[]; +}; + +type SortBaseOptions = { + /** + * Limiting the range of the query by setting offset and result count. See {@link Limit} class for + * more information. + */ + limit?: Limit; + + /** Options for sorting order of elements. */ + orderBy?: SortOrder; + + /** + * When `true`, sorts elements lexicographically. When `false` (default), + * sorts elements numerically. Use this when the list, set, or sorted set contains string values + * that cannot be converted into double precision floating point numbers. + */ + isAlpha?: boolean; +}; + +/** + * Optional arguments to {@link GlideClusterClient.sort|sort}, {@link GlideClusterClient.sortStore|sortStore} and {@link GlideClusterClient.sortReadOnly|sortReadOnly} commands. + * + * See https://valkey.io/commands/sort/ for more details. + */ +export type SortClusterOptions = SortBaseOptions; + +/** + * The `LIMIT` argument is commonly used to specify a subset of results from the + * matching elements, similar to the `LIMIT` clause in SQL (e.g., `SELECT LIMIT offset, count`). + */ +export type Limit = { + /** The starting position of the range, zero based. */ + offset: number; + /** The maximum number of elements to include in the range. A negative count returns all elements from the offset. */ + count: number; +}; + +/** @internal */ +export function createSort( + key: string, + options?: SortOptions, + destination?: string, +): command_request.Command { + return createSortImpl(RequestType.Sort, key, options, destination); +} + +/** @internal */ +export function createSortReadOnly( + key: string, + options?: SortOptions, +): command_request.Command { + return createSortImpl(RequestType.SortReadOnly, key, options); +} + +/** @internal */ +function createSortImpl( + cmd: RequestType, + key: string, + options?: SortOptions, + destination?: string, +): command_request.Command { + const args: string[] = [key]; + + if (options) { + if (options.limit) { + args.push( + "LIMIT", + options.limit.offset.toString(), + options.limit.count.toString(), + ); + } + + if (options.orderBy) { + args.push(options.orderBy); + } + + if (options.isAlpha) { + args.push("ALPHA"); + } + + if (options.byPattern) { + args.push("BY", options.byPattern); + } + + if (options.getPatterns) { + options.getPatterns.forEach((p) => args.push("GET", p)); + } + } + + if (destination) args.push("STORE", destination); + + return createCommand(cmd, args); +} + /** * @internal */ diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index aaeb277b60..7bd383c320 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -7,6 +7,7 @@ import { BaseClient, BaseClientConfiguration, PubSubMsg, + ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars ReturnType, } from "./BaseClient"; import { @@ -15,6 +16,7 @@ import { FunctionListResponse, InfoOptions, LolwutOptions, + SortOptions, createClientGetName, createClientId, createConfigGet, @@ -38,6 +40,8 @@ import { createPublish, createRandomKey, createSelect, + createSort, + createSortReadOnly, createTime, } from "./Commands"; import { connection_request } from "./ProtobufMessage"; @@ -612,6 +616,101 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createPublish(message, channel)); } + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * To store the result into a new key, see {@link sortStore}. + * + * See https://valkey.io/commands/sort for more details. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - The {@link SortOptions}. + * @returns An `Array` of sorted elements. + * + * @example + * ```typescript + * await client.hset("user:1", new Map([["name", "Alice"], ["age", "30"]])); + * await client.hset("user:2", new Map([["name", "Bob"], ["age", "25"]])); + * await client.lpush("user_ids", ["2", "1"]); + * const result = await client.sort("user_ids", { byPattern: "user:*->age", getPattern: ["user:*->name"] }); + * console.log(result); // Output: [ 'Bob', 'Alice' ] - Returns a list of the names sorted by age + * ``` + */ + public async sort( + key: string, + options?: SortOptions, + ): Promise<(string | null)[]> { + return this.createWritePromise(createSort(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sortReadOnly` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * since Valkey version 7.0.0. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - The {@link SortOptions}. + * @returns An `Array` of sorted elements + * + * @example + * ```typescript + * await client.hset("user:1", new Map([["name", "Alice"], ["age", "30"]])); + * await client.hset("user:2", new Map([["name", "Bob"], ["age", "25"]])); + * await client.lpush("user_ids", ["2", "1"]); + * const result = await client.sortReadOnly("user_ids", { byPattern: "user:*->age", getPattern: ["user:*->name"] }); + * console.log(result); // Output: [ 'Bob', 'Alice' ] - Returns a list of the names sorted by age + * ``` + */ + public async sortReadOnly( + key: string, + options?: SortOptions, + ): Promise<(string | null)[]> { + return this.createWritePromise(createSortReadOnly(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and stores the result in + * `destination`. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements, and store the result in a new key. + * + * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. + * + * See https://valkey.io/commands/sort for more details. + * + * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. + * @param key - The key of the list, set, or sorted set to be sorted. + * @param destination - The key where the sorted result will be stored. + * @param options - The {@link SortOptions}. + * @returns The number of elements in the sorted key stored at `destination`. + * + * @example + * ```typescript + * await client.hset("user:1", new Map([["name", "Alice"], ["age", "30"]])); + * await client.hset("user:2", new Map([["name", "Bob"], ["age", "25"]])); + * await client.lpush("user_ids", ["2", "1"]); + * const sortedElements = await client.sortStore("user_ids", "sortedList", { byPattern: "user:*->age", getPattern: ["user:*->name"] }); + * console.log(sortedElements); // Output: 2 - number of elements sorted and stored + * console.log(await client.lrange("sortedList", 0, -1)); // Output: [ 'Bob', 'Alice' ] - Returns a list of the names sorted by age stored in `sortedList` + * ``` + */ + public async sortStore( + key: string, + destination: string, + options?: SortOptions, + ): Promise { + return this.createWritePromise(createSort(key, options, destination)); + } + /** * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save * was made since then. diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index d01459d970..a95d3bdfa7 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -7,6 +7,7 @@ import { BaseClient, BaseClientConfiguration, PubSubMsg, + ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars ReturnType, } from "./BaseClient"; import { @@ -15,6 +16,7 @@ import { FunctionListResponse, InfoOptions, LolwutOptions, + SortClusterOptions, createClientGetName, createClientId, createConfigGet, @@ -38,6 +40,8 @@ import { createLolwut, createPing, createPublish, + createSort, + createSortReadOnly, createRandomKey, createTime, } from "./Commands"; @@ -986,6 +990,95 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * To store the result into a new key, see {@link sortStore}. + * + * See https://valkey.io/commands/sort for more details. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortClusterOptions}. + * @returns An `Array` of sorted elements. + * + * @example + * ```typescript + * await client.lpush("mylist", ["3", "1", "2", "a"]); + * const result = await client.sort("mylist", { alpha: true, orderBy: SortOrder.DESC, limit: { offset: 0, count: 3 } }); + * console.log(result); // Output: [ 'a', '3', '2' ] - List is sorted in descending order lexicographically + * ``` + */ + public async sort( + key: string, + options?: SortClusterOptions, + ): Promise { + return this.createWritePromise(createSort(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sortReadOnly` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * since Valkey version 7.0.0. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortClusterOptions}. + * @returns An `Array` of sorted elements + * + * @example + * ```typescript + * await client.lpush("mylist", ["3", "1", "2", "a"]); + * const result = await client.sortReadOnly("mylist", { alpha: true, orderBy: SortOrder.DESC, limit: { offset: 0, count: 3 } }); + * console.log(result); // Output: [ 'a', '3', '2' ] - List is sorted in descending order lexicographically + * ``` + */ + public async sortReadOnly( + key: string, + options?: SortClusterOptions, + ): Promise { + return this.createWritePromise(createSortReadOnly(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and stores the result in + * `destination`. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements, and store the result in a new key. + * + * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. + * + * See https://valkey.io/commands/sort for more details. + * + * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. + * @param key - The key of the list, set, or sorted set to be sorted. + * @param destination - The key where the sorted result will be stored. + * @param options - (Optional) {@link SortClusterOptions}. + * @returns The number of elements in the sorted key stored at `destination`. + * + * @example + * ```typescript + * await client.lpush("mylist", ["3", "1", "2", "a"]); + * const sortedElements = await client.sortReadOnly("mylist", "sortedList", { alpha: true, orderBy: SortOrder.DESC, limit: { offset: 0, count: 3 } }); + * console.log(sortedElements); // Output: 3 - number of elements sorted and stored + * console.log(await client.lrange("sortedList", 0, -1)); // Output: [ 'a', '3', '2' ] - List is sorted in descending order lexicographically and stored in `sortedList` + * ``` + */ + public async sortStore( + key: string, + destination: string, + options?: SortClusterOptions, + ): Promise { + return this.createWritePromise(createSort(key, options, destination)); + } + /** * Returns `UNIX TIME` of the last DB save timestamp or startup timestamp if no save * was made since then. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 79d6d7362b..37cb7b380b 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -2,6 +2,10 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +import { + ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars +} from "./BaseClient"; + import { AggregationType, BitFieldGet, @@ -188,6 +192,10 @@ import { createZRevRank, createZRevRankWithScore, createZScore, + createSort, + SortOptions, + createSortReadOnly, + SortClusterOptions, createLastSave, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -2699,6 +2707,70 @@ export class Transaction extends BaseTransaction { return this.addAndReturn(createSelect(index)); } + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * To store the result into a new key, see {@link sortStore}. + * + * See https://valkey.io/commands/sort for more details. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortOptions}. + * + * Command Response - An `Array` of sorted elements. + */ + public sort(key: string, options?: SortOptions): Transaction { + return this.addAndReturn(createSort(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sortReadOnly` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * since Valkey version 7.0.0. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortOptions}. + * + * Command Response - An `Array` of sorted elements + */ + public sortReadOnly(key: string, options?: SortOptions): Transaction { + return this.addAndReturn(createSortReadOnly(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and stores the result in + * `destination`. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements, and store the result in a new key. + * + * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. + * + * See https://valkey.io/commands/sort for more details. + * + * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. + * @param key - The key of the list, set, or sorted set to be sorted. + * @param destination - The key where the sorted result will be stored. + * @param options - (Optional) {@link SortOptions}. + * + * Command Response - The number of elements in the sorted key stored at `destination`. + */ + public sortStore( + key: string, + destination: string, + options?: SortOptions, + ): Transaction { + return this.addAndReturn(createSort(key, options, destination)); + } + /** * Copies the value stored at the `source` to the `destination` key. If `destinationDB` is specified, * the value will be copied to the database specified, otherwise the current database will be used. @@ -2741,6 +2813,73 @@ export class Transaction extends BaseTransaction { export class ClusterTransaction extends BaseTransaction { /// TODO: add all CLUSTER commands + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * To store the result into a new key, see {@link sortStore}. + * + * See https://valkey.io/commands/sort for more details. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortClusterOptions}. + * + * Command Response - An `Array` of sorted elements. + */ + public sort(key: string, options?: SortClusterOptions): ClusterTransaction { + return this.addAndReturn(createSort(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and returns the result. + * + * The `sortReadOnly` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements. + * + * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * since Valkey version 7.0.0. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - (Optional) {@link SortClusterOptions}. + * + * Command Response - An `Array` of sorted elements + */ + public sortReadOnly( + key: string, + options?: SortClusterOptions, + ): ClusterTransaction { + return this.addAndReturn(createSortReadOnly(key, options)); + } + + /** + * Sorts the elements in the list, set, or sorted set at `key` and stores the result in + * `destination`. + * + * The `sort` command can be used to sort elements based on different criteria and + * apply transformations on sorted elements, and store the result in a new key. + * + * To get the sort result without storing it into a key, see {@link sort} or {@link sortReadOnly}. + * + * See https://valkey.io/commands/sort for more details. + * + * @remarks When in cluster mode, `destination` and `key` must map to the same hash slot. + * @param key - The key of the list, set, or sorted set to be sorted. + * @param destination - The key where the sorted result will be stored. + * @param options - (Optional) {@link SortClusterOptions}. + * + * Command Response - The number of elements in the sorted key stored at `destination`. + */ + public sortStore( + key: string, + destination: string, + options?: SortClusterOptions, + ): ClusterTransaction { + return this.addAndReturn(createSort(key, options, destination)); + } + /** * Copies the value stored at the `source` to the `destination` key. When `replace` is true, * removes the `destination` key first if it already exists, otherwise performs no action. diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index f6d7435e83..c168284057 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -14,7 +14,7 @@ import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; import { GlideClient, ListDirection, ProtocolVersion, Transaction } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode } from "../build-ts/src/Commands"; +import { FlushMode, SortOrder } from "../build-ts/src/Commands"; import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; import { @@ -668,6 +668,232 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "sort sortstore sort_store sortro sort_ro sortreadonly test_%p", + async (protocol) => { + const client = await GlideClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const setPrefix = "setKey" + uuidv4(); + const hashPrefix = "hashKey" + uuidv4(); + const list = uuidv4(); + const store = uuidv4(); + const names = ["Alice", "Bob", "Charlie", "Dave", "Eve"]; + const ages = ["30", "25", "35", "20", "40"]; + + for (let i = 0; i < ages.length; i++) { + expect( + await client.hset(setPrefix + (i + 1), { + name: names[i], + age: ages[i], + }), + ).toEqual(2); + } + + expect(await client.rpush(list, ["3", "1", "5", "4", "2"])).toEqual( + 5, + ); + + expect( + await client.sort(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual(["Alice", "Bob"]); + + expect( + await client.sort(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + orderBy: SortOrder.DESC, + }), + ).toEqual(["Eve", "Dave"]); + + expect( + await client.sort(list, { + limit: { offset: 0, count: 2 }, + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name", setPrefix + "*->age"], + orderBy: SortOrder.DESC, + }), + ).toEqual(["Eve", "40", "Charlie", "35"]); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + expect(await client.sort(list, { byPattern: "noSort" })).toEqual([ + "3", + "1", + "5", + "4", + "2", + ]); + + // Non-existent key in the GET pattern results in nulls + expect( + await client.sort(list, { + isAlpha: true, + getPatterns: ["missing"], + }), + ).toEqual([null, null, null, null, null]); + + // Missing key in the set + expect(await client.lpush(list, ["42"])).toEqual(6); + expect( + await client.sort(list, { + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual([null, "Dave", "Bob", "Alice", "Charlie", "Eve"]); + expect(await client.lpop(list)).toEqual("42"); + + // sort RO + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + expect( + await client.sortReadOnly(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual(["Alice", "Bob"]); + + expect( + await client.sortReadOnly(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + orderBy: SortOrder.DESC, + }), + ).toEqual(["Eve", "Dave"]); + + expect( + await client.sortReadOnly(list, { + limit: { offset: 0, count: 2 }, + byPattern: setPrefix + "*->age", + getPatterns: [ + setPrefix + "*->name", + setPrefix + "*->age", + ], + orderBy: SortOrder.DESC, + }), + ).toEqual(["Eve", "40", "Charlie", "35"]); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + expect( + await client.sortReadOnly(list, { byPattern: "noSort" }), + ).toEqual(["3", "1", "5", "4", "2"]); + + // Non-existent key in the GET pattern results in nulls + expect( + await client.sortReadOnly(list, { + isAlpha: true, + getPatterns: ["missing"], + }), + ).toEqual([null, null, null, null, null]); + + // Missing key in the set + expect(await client.lpush(list, ["42"])).toEqual(6); + expect( + await client.sortReadOnly(list, { + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual([null, "Dave", "Bob", "Alice", "Charlie", "Eve"]); + expect(await client.lpop(list)).toEqual("42"); + } + + // SORT with STORE + expect( + await client.sortStore(list, store, { + limit: { offset: 0, count: -1 }, + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name"], + orderBy: SortOrder.ASC, + }), + ).toEqual(5); + expect(await client.lrange(store, 0, -1)).toEqual([ + "Dave", + "Bob", + "Alice", + "Charlie", + "Eve", + ]); + expect( + await client.sortStore(list, store, { + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual(5); + expect(await client.lrange(store, 0, -1)).toEqual([ + "Dave", + "Bob", + "Alice", + "Charlie", + "Eve", + ]); + + // transaction test + const transaction = new Transaction() + .hset(hashPrefix + 1, { name: "Alice", age: "30" }) + .hset(hashPrefix + 2, { name: "Bob", age: "25" }) + .del([list]) + .lpush(list, ["2", "1"]) + .sort(list, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + }) + .sort(list, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + orderBy: SortOrder.DESC, + }) + .sortStore(list, store, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + }) + .lrange(store, 0, -1) + .sortStore(list, store, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + orderBy: SortOrder.DESC, + }) + .lrange(store, 0, -1); + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + transaction + .sortReadOnly(list, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + }) + .sortReadOnly(list, { + byPattern: hashPrefix + "*->age", + getPatterns: [hashPrefix + "*->name"], + orderBy: SortOrder.DESC, + }); + } + + const expectedResult = [ + 2, + 2, + 1, + 2, + ["Bob", "Alice"], + ["Alice", "Bob"], + 2, + ["Bob", "Alice"], + 2, + ["Alice", "Bob"], + ]; + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + expectedResult.push(["Bob", "Alice"], ["Alice", "Bob"]); + } + + const result = await client.exec(transaction); + expect(result).toEqual(expectedResult); + + client.close(); + }, + TIMEOUT, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "randomKey test_%p", async (protocol) => { diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 91af181a74..7421d7bb3c 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -20,11 +20,12 @@ import { InfoOptions, ListDirection, ProtocolVersion, + RequestError, Routes, ScoreFilter, } from ".."; +import { FlushMode, SortOrder } from "../build-ts/src/Commands"; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode } from "../build-ts/src/Commands"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, @@ -322,6 +323,8 @@ describe("GlideClusterClient", () => { client.pfmerge("abc", ["def", "ghi"]), client.sdiff(["abc", "zxy", "lkn"]), client.sdiffstore("abc", ["zxy", "lkn"]), + client.sortStore("abc", "zyx"), + client.sortStore("abc", "zyx", { isAlpha: true }), ]; if (gte(cluster.getVersion(), "6.2.0")) { @@ -624,6 +627,90 @@ describe("GlideClusterClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "sort sortstore sort_store sortro sort_ro sortreadonly test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const key1 = "{sort}" + uuidv4(); + const key2 = "{sort}" + uuidv4(); + const key3 = "{sort}" + uuidv4(); + const key4 = "{sort}" + uuidv4(); + const key5 = "{sort}" + uuidv4(); + + expect(await client.sort(key3)).toEqual([]); + expect(await client.lpush(key1, ["2", "1", "4", "3"])).toEqual(4); + expect(await client.sort(key1)).toEqual(["1", "2", "3", "4"]); + + // sort RO + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.sortReadOnly(key3)).toEqual([]); + expect(await client.sortReadOnly(key1)).toEqual([ + "1", + "2", + "3", + "4", + ]); + } + + // sort with store + expect(await client.sortStore(key1, key2)).toEqual(4); + expect(await client.lrange(key2, 0, -1)).toEqual([ + "1", + "2", + "3", + "4", + ]); + + // SORT with strings require ALPHA + expect( + await client.rpush(key3, ["2", "1", "a", "x", "c", "4", "3"]), + ).toEqual(7); + await expect(client.sort(key3)).rejects.toThrow(RequestError); + expect(await client.sort(key3, { isAlpha: true })).toEqual([ + "1", + "2", + "3", + "4", + "a", + "c", + "x", + ]); + + // check transaction and options + const transaction = new ClusterTransaction() + .lpush(key4, ["3", "1", "2"]) + .sort(key4, { + orderBy: SortOrder.DESC, + limit: { count: 2, offset: 0 }, + }) + .sortStore(key4, key5, { + orderBy: SortOrder.ASC, + limit: { count: 100, offset: 1 }, + }) + .lrange(key5, 0, -1); + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + transaction.sortReadOnly(key4, { + orderBy: SortOrder.DESC, + limit: { count: 2, offset: 0 }, + }); + } + + const result = await client.exec(transaction); + const expectedResult = [3, ["3", "2"], 2, ["2", "3"]]; + + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + expectedResult.push(["3", "2"]); + } + + expect(result).toEqual(expectedResult); + + client.close(); + }, + ); + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "Protocol is RESP2 = %s", (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 99f53b854d..e87deb5c21 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -468,7 +468,9 @@ export async function transactionTest( const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET const key19 = "{key}" + uuidv4(); // bitmap const key20 = "{key}" + uuidv4(); // list - const key21 = "{key}" + uuidv4(); // zset random + const key21 = "{key}" + uuidv4(); // list for sort + const key22 = "{key}" + uuidv4(); // list for sort + const key23 = "{key}" + uuidv4(); // zset random const field = uuidv4(); const value = uuidv4(); // array of tuples - first element is test name/description, second - expected return value @@ -939,15 +941,15 @@ export async function transactionTest( 'geohash(key18, ["Palermo", "Catania", "NonExisting"])', ["sqc8b49rny0", "sqdtr74hyu0", null], ]); - baseTransaction.zadd(key21, { one: 1.0 }); - responseData.push(["zadd(key21, {one: 1.0}", 1]); - baseTransaction.zrandmember(key21); - responseData.push(["zrandmember(key21)", "one"]); - baseTransaction.zrandmemberWithCount(key21, 1); - responseData.push(["zrandmemberWithCount(key21, 1)", ["one"]]); - baseTransaction.zrandmemberWithCountWithScores(key21, 1); + baseTransaction.zadd(key23, { one: 1.0 }); + responseData.push(["zadd(key23, {one: 1.0}", 1]); + baseTransaction.zrandmember(key23); + responseData.push(["zrandmember(key23)", "one"]); + baseTransaction.zrandmemberWithCount(key23, 1); + responseData.push(["zrandmemberWithCount(key23, 1)", ["one"]]); + baseTransaction.zrandmemberWithCountWithScores(key23, 1); responseData.push([ - "zrandmemberWithCountWithScores(key21, 1)", + "zrandmemberWithCountWithScores(key23, 1)", [["one", 1.0]], ]); @@ -1130,5 +1132,22 @@ export async function transactionTest( ); } + baseTransaction + .lpush(key21, ["3", "1", "2"]) + .sort(key21) + .sortStore(key21, key22) + .lrange(key22, 0, -1); + responseData.push( + ['lpush(key21, ["3", "1", "2"])', 3], + ["sort(key21)", ["1", "2", "3"]], + ["sortStore(key21, key22)", 3], + ["lrange(key22, 0, -1)", ["1", "2", "3"]], + ); + + if (gte("7.0.0", version)) { + baseTransaction.sortReadOnly(key21); + responseData.push(["sortReadOnly(key21)", ["1", "2", "3"]]); + } + return responseData; }