From 48813250c8109fcb8f5f7cf9c0dd0d4029572a94 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 26 Jul 2024 16:59:46 -0700 Subject: [PATCH 1/4] Add `SORT` command. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 2 +- node/src/Commands.ts | 138 ++++++++++++++++ node/src/GlideClient.ts | 95 +++++++++++ node/src/GlideClusterClient.ts | 89 ++++++++++ node/src/Transaction.ts | 131 +++++++++++++++ node/tests/RedisClient.test.ts | 223 +++++++++++++++++++++++++- node/tests/RedisClusterClient.test.ts | 89 +++++++++- node/tests/TestUtilities.ts | 19 +++ 9 files changed, 784 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2e4b5e71..df8cc38121 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 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)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 87a2df8531..d5b1bd4d8c 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -200,7 +200,7 @@ type RedisCredentials = { password: string; }; -type ReadFrom = +export type ReadFrom = /** Always get from primary, in order to get the freshest data.*/ | "primary" /** Spread the requests between all replicas in a round robin manner. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index ff4eac85ae..25a383b981 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2225,3 +2225,141 @@ export function createZIncrBy( member, ]); } + +/** + * Optional arguments to {@link GlideClient.sort|sort}, {@link GlideClient.sortStore|sortStore} and {@link GlideClient.sortReadOnly|sortReadOnly} commands. + * + * See https://valkey.io/commands/sort/ and https://valkey.io/commands/sort_ro/ for more details. + */ +export type SortOptions = SortClusterOptions & { + /** + * 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.
+ * + * See https://valkey.io/commands/sort/ for more information. + */ + getPatterns?: string[]; +}; + +/** + * Optional arguments to {@link GlideClusterClient.sort|sort}, {@link GlideClusterClient.sortStore|sortStore} and {@link GlideClusterClient.sortReadOnly|sortReadOnly} commands. + * + * See https://valkey.io/commands/sort/ and https://valkey.io/commands/sort_ro/ for more details. + */ +export type SortClusterOptions = { + /** + * 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; +}; + +/** + * 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; +}; + +/** Defines the sort order for nested results. */ +export enum SortOrder { + /** Sort by ascending order. */ + ASC = "ASC", + /** Sort by descending order. */ + DESC = "DESC", +} + +/** + * @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); +} diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index c63715b6df..9e3c8e7b07 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -7,12 +7,14 @@ import { BaseClient, BaseClientConfiguration, PubSubMsg, + ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars ReturnType, } from "./BaseClient"; import { FlushMode, InfoOptions, LolwutOptions, + SortOptions, createClientGetName, createClientId, createConfigGet, @@ -32,6 +34,8 @@ import { createPing, createPublish, createSelect, + createSort, + createSortReadOnly, createTime, } from "./Commands"; import { connection_request } from "./ProtobufMessage"; @@ -528,4 +532,95 @@ export class GlideClient extends BaseClient { public publish(message: string, channel: string): Promise { 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}. + * + * @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}. + * + * @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)); + } } diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 632132a3e0..7a15dbf55e 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -7,12 +7,14 @@ import { BaseClient, BaseClientConfiguration, PubSubMsg, + ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars ReturnType, } from "./BaseClient"; import { FlushMode, InfoOptions, LolwutOptions, + SortClusterOptions, createClientGetName, createClientId, createConfigGet, @@ -33,6 +35,8 @@ import { createLolwut, createPing, createPublish, + createSort, + createSortReadOnly, createTime, } from "./Commands"; import { RequestError } from "./Errors"; @@ -907,4 +911,89 @@ export class GlideClusterClient extends BaseClient { createPublish(message, channel, sharded), ); } + + /** + * 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}. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - The {@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 - The {@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}. + * + * @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 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)); + } } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index df3a75afd1..9268ae2404 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, BitOffsetOptions, @@ -157,6 +161,10 @@ import { createZRevRankWithScore, createZScore, createZIncrBy, + createSort, + SortOptions, + createSortReadOnly, + SortClusterOptions, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -2270,6 +2278,66 @@ export class Transaction extends BaseTransaction { public select(index: number): Transaction { 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}. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - The {@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 - The {@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}. + * + * @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}. + * + * 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)); + } } /** @@ -2285,4 +2353,67 @@ 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}. + * + * @param key - The key of the list, set, or sorted set to be sorted. + * @param options - The {@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 - The {@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}. + * + * @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 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)); + } } diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index e8326ab797..d6933421d1 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, 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 { @@ -555,6 +555,227 @@ 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, + ); + + checkSimple( + await client.sort(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual(["Alice", "Bob"]); + + checkSimple( + await client.sort(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + orderBy: SortOrder.DESC, + }), + ).toEqual(["Eve", "Dave"]); + + checkSimple( + 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 + checkSimple( + await client.sort(list, { byPattern: "noSort" }), + ).toEqual(["3", "1", "5", "4", "2"]); + + // Non-existent key in the GET pattern results in nulls + checkSimple( + 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); + checkSimple( + await client.sort(list, { + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual([null, "Dave", "Bob", "Alice", "Charlie", "Eve"]); + checkSimple(await client.lpop(list)).toEqual("42"); + + // sort RO + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + checkSimple( + await client.sortReadOnly(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual(["Alice", "Bob"]); + + checkSimple( + await client.sortReadOnly(list, { + limit: { offset: 0, count: 2 }, + getPatterns: [setPrefix + "*->name"], + orderBy: SortOrder.DESC, + }), + ).toEqual(["Eve", "Dave"]); + + checkSimple( + 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 + checkSimple( + await client.sortReadOnly(list, { byPattern: "noSort" }), + ).toEqual(["3", "1", "5", "4", "2"]); + + // Non-existent key in the GET pattern results in nulls + checkSimple( + 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); + checkSimple( + await client.sortReadOnly(list, { + byPattern: setPrefix + "*->age", + getPatterns: [setPrefix + "*->name"], + }), + ).toEqual([null, "Dave", "Bob", "Alice", "Charlie", "Eve"]); + checkSimple(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); + checkSimple(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); + checkSimple(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); + checkSimple(result).toEqual(expectedResult); + + client.close(); + }, + ); + runBaseTests({ init: async (protocol, clientName?) => { const options = getClientConfigurationOption( diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 79713181a2..51f14c8e72 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -18,10 +18,11 @@ import { GlideClusterClient, InfoOptions, ProtocolVersion, + RequestError, Routes, ScoreFilter, } from ".."; -import { FlushMode } from "../build-ts/src/Commands"; +import { FlushMode, SortOrder } from "../build-ts/src/Commands"; import { RedisCluster } from "../../utils/TestUtils.js"; import { runBaseTests } from "./SharedTests"; import { @@ -318,6 +319,8 @@ describe("GlideClusterClient", () => { client.pfcount(["abc", "zxy", "lkn"]), 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")) { @@ -564,6 +567,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); + checkSimple(await client.sort(key1)).toEqual(["1", "2", "3", "4"]); + + // sort RO + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.sortReadOnly(key3)).toEqual([]); + checkSimple(await client.sortReadOnly(key1)).toEqual([ + "1", + "2", + "3", + "4", + ]); + } + + // sort with store + expect(await client.sortStore(key1, key2)).toEqual(4); + checkSimple(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); + checkSimple(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"]); + } + + checkSimple(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 9f23326bc6..1c22de9ab7 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -397,6 +397,8 @@ 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(); // list for sort + const key22 = "{key}" + uuidv4(); // list for sort const field = uuidv4(); const value = uuidv4(); // array of tuples - first element is test name/description, second - expected return value @@ -826,5 +828,22 @@ export async function transactionTest( responseData.push(["functionFlush(FlushMode.SYNC)", "OK"]); } + 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; } From f1c29ce85dd3a581245f1e505d4e2e5a147d2bc1 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 30 Jul 2024 13:17:24 -0700 Subject: [PATCH 2/4] Fix links. Signed-off-by: Yury-Fridlyand --- node/src/Commands.ts | 8 +++----- node/src/GlideClient.ts | 4 ++++ node/src/GlideClusterClient.ts | 4 ++++ node/src/Transaction.ts | 8 ++++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 581c5205c3..b39fd2e146 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2424,7 +2424,7 @@ 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/ and https://valkey.io/commands/sort_ro/ for more details. + * See https://valkey.io/commands/sort/ for more details. */ export type SortOptions = SortClusterOptions & { /** @@ -2447,9 +2447,7 @@ export type SortOptions = SortClusterOptions & { * `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.
- * - * See https://valkey.io/commands/sort/ for more information. + * the sorted elements themselves are returned. */ getPatterns?: string[]; }; @@ -2475,7 +2473,7 @@ type SortBaseOptions = { /** * Optional arguments to {@link GlideClusterClient.sort|sort}, {@link GlideClusterClient.sortStore|sortStore} and {@link GlideClusterClient.sortReadOnly|sortReadOnly} commands. * - * See https://valkey.io/commands/sort/ and https://valkey.io/commands/sort_ro/ for more details. + * See https://valkey.io/commands/sort/ for more details. */ export type SortClusterOptions = SortBaseOptions; diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index f57f8d555b..5e545f5061 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -622,6 +622,8 @@ export class GlideClient extends BaseClient { * * 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. @@ -681,6 +683,8 @@ export class GlideClient extends BaseClient { * * 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. diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index b681f7e324..1656e3ca94 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -996,6 +996,8 @@ export class GlideClusterClient extends BaseClient { * * 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 SortClusterOptions}. * @returns An `Array` of sorted elements. @@ -1051,6 +1053,8 @@ export class GlideClusterClient extends BaseClient { * * 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. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 084f14cb20..b8af302cbe 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -2454,6 +2454,8 @@ export class Transaction extends BaseTransaction { * * 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}. * @@ -2491,6 +2493,8 @@ export class Transaction extends BaseTransaction { * * 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. @@ -2556,6 +2560,8 @@ export class ClusterTransaction extends BaseTransaction { * * 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 SortClusterOptions}. * @@ -2596,6 +2602,8 @@ export class ClusterTransaction extends BaseTransaction { * * 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. From 27818533825bff93511122e7846d7b1282dead21 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Tue, 30 Jul 2024 13:33:16 -0700 Subject: [PATCH 3/4] Signed-off-by: Yury-Fridlyand --- node/src/GlideClusterClient.ts | 6 +++--- node/src/Transaction.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 1656e3ca94..f0e29b242d 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -999,7 +999,7 @@ export class GlideClusterClient extends BaseClient { * 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 SortClusterOptions}. + * @param options - (Optional) {@link SortClusterOptions}. * @returns An `Array` of sorted elements. * * @example @@ -1027,7 +1027,7 @@ export class GlideClusterClient extends BaseClient { * since Valkey version 7.0.0. * * @param key - The key of the list, set, or sorted set to be sorted. - * @param options - The {@link SortClusterOptions}. + * @param options - (Optional) {@link SortClusterOptions}. * @returns An `Array` of sorted elements * * @example @@ -1058,7 +1058,7 @@ export class GlideClusterClient extends BaseClient { * @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 SortClusterOptions}. + * @param options - (Optional) {@link SortClusterOptions}. * @returns The number of elements in the sorted key stored at `destination`. * * @example diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index b8af302cbe..b577a69698 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -2457,7 +2457,7 @@ export class Transaction extends BaseTransaction { * 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}. + * @param options - (Optional) {@link SortOptions}. * * Command Response - An `Array` of sorted elements. */ @@ -2476,7 +2476,7 @@ export class Transaction extends BaseTransaction { * 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}. + * @param options - (Optional) {@link SortOptions}. * * Command Response - An `Array` of sorted elements */ @@ -2498,7 +2498,7 @@ export class Transaction extends BaseTransaction { * @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}. + * @param options - (Optional) {@link SortOptions}. * * Command Response - The number of elements in the sorted key stored at `destination`. */ @@ -2563,7 +2563,7 @@ export class ClusterTransaction extends BaseTransaction { * 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 SortClusterOptions}. + * @param options - (Optional) {@link SortClusterOptions}. * * Command Response - An `Array` of sorted elements. */ @@ -2582,7 +2582,7 @@ export class ClusterTransaction extends BaseTransaction { * since Valkey version 7.0.0. * * @param key - The key of the list, set, or sorted set to be sorted. - * @param options - The {@link SortClusterOptions}. + * @param options - (Optional) {@link SortClusterOptions}. * * Command Response - An `Array` of sorted elements */ @@ -2607,7 +2607,7 @@ export class ClusterTransaction extends BaseTransaction { * @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 SortClusterOptions}. + * @param options - (Optional) {@link SortClusterOptions}. * * Command Response - The number of elements in the sorted key stored at `destination`. */ From 80d565854e5c2a6bcc2200e0e23e333e71f7aba6 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 31 Jul 2024 17:33:59 -0700 Subject: [PATCH 4/4] Update node/src/Commands.ts Co-authored-by: Andrew Carbonetto Signed-off-by: Yury-Fridlyand --- node/src/Commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/Commands.ts b/node/src/Commands.ts index d1d69ed290..f569a5a3e9 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2793,7 +2793,7 @@ export function createZIncrBy( * * See https://valkey.io/commands/sort/ for more details. */ -export type SortOptions = SortClusterOptions & { +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