From d227806d60d2c3c71ff7608b7fac2bad651d8a89 Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Wed, 19 Jun 2024 21:02:10 +0000 Subject: [PATCH] Java: Add `SORT` and `SORT_RO` commands (#363) * Added test * Added child classes * Needs cleaning up and store needs separate functions * Simplified sort apis * Separated sortOptions and sortStandAloneOptions * Added tests * Added IT and UT * Revert to Builder from SuperBuilder * Added javadocs * Added transaction tests * Addressed comments * Spotless * Added SortBaseOptions * Addressed docs comments * Fixed comment * Refactor sortWithStore to sortStore * Addressed comments * Update java/client/src/main/java/glide/api/commands/GenericBaseCommands.java Co-authored-by: Yury-Fridlyand --------- Co-authored-by: Yury-Fridlyand --- glide-core/src/protobuf/redis_request.proto | 2 + glide-core/src/request_type.rs | 6 + .../src/main/java/glide/api/BaseClient.java | 53 ++++ .../src/main/java/glide/api/RedisClient.java | 34 +++ .../api/commands/GenericBaseCommands.java | 148 ++++++++++ .../glide/api/commands/GenericCommands.java | 91 ++++++ .../glide/api/models/BaseTransaction.java | 114 ++++++++ .../java/glide/api/models/Transaction.java | 70 +++++ .../api/models/commands/SortBaseOptions.java | 13 + .../api/models/commands/SortOptions.java | 108 ++++++++ .../commands/SortStandaloneOptions.java | 75 +++++ .../test/java/glide/api/RedisClientTest.java | 261 ++++++++++++++++++ .../glide/api/RedisClusterClientTest.java | 199 +++++++++++++ .../models/StandaloneTransactionTests.java | 143 ++++++++++ .../glide/api/models/TransactionTests.java | 59 ++++ .../test/java/glide/SharedCommandTests.java | 95 +++++++ .../java/glide/TransactionTestUtilities.java | 28 +- .../java/glide/standalone/CommandTests.java | 209 ++++++++++++++ .../glide/standalone/TransactionTests.java | 88 ++++++ 19 files changed, 1794 insertions(+), 2 deletions(-) create mode 100644 java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java create mode 100644 java/client/src/main/java/glide/api/models/commands/SortOptions.java create mode 100644 java/client/src/main/java/glide/api/models/commands/SortStandaloneOptions.java diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index f5ad60991c..24734efafa 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -206,6 +206,8 @@ enum RequestType { BitFieldReadOnly = 173; Move = 174; SInterCard = 175; + Sort = 176; + SortReadOnly = 177; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 7497cce94d..5dff21c3dd 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -176,6 +176,8 @@ pub enum RequestType { BitFieldReadOnly = 173, Move = 174, SInterCard = 175, + Sort = 176, + SortReadOnly = 177, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -355,6 +357,8 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::BitFieldReadOnly => RequestType::BitFieldReadOnly, ProtobufRequestType::Move => RequestType::Move, ProtobufRequestType::SInterCard => RequestType::SInterCard, + ProtobufRequestType::Sort => RequestType::Sort, + ProtobufRequestType::SortReadOnly => RequestType::SortReadOnly, } } } @@ -530,6 +534,8 @@ impl RequestType { RequestType::BitFieldReadOnly => Some(cmd("BITFIELD_RO")), RequestType::Move => Some(cmd("MOVE")), RequestType::SInterCard => Some(cmd("SINTERCARD")), + RequestType::Sort => Some(cmd("SORT")), + RequestType::SortReadOnly => Some(cmd("SORT_RO")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 1de4f2d2be..2b10cf7ee2 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; @@ -105,6 +106,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Set; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Touch; @@ -164,6 +167,7 @@ import glide.api.models.commands.ScoreFilter; import glide.api.models.commands.ScriptOptions; import glide.api.models.commands.SetOptions; +import glide.api.models.commands.SortBaseOptions; import glide.api.models.commands.WeightAggregateOptions.Aggregate; import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeys; import glide.api.models.commands.ZAddOptions; @@ -1677,4 +1681,53 @@ public CompletableFuture sintercard(@NonNull String[] keys, long limit) { new String[] {SET_LIMIT_REDIS_API, Long.toString(limit)}); return commandManager.submitNewCommand(SInterCard, arguments, this::handleLongResponse); } + + @Override + public CompletableFuture sort(@NonNull String key) { + return commandManager.submitNewCommand( + Sort, + new String[] {key}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sort( + @NonNull String key, @NonNull SortBaseOptions sortBaseOptions) { + String[] arguments = ArrayUtils.addFirst(sortBaseOptions.toArgs(), key); + return commandManager.submitNewCommand( + Sort, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly(@NonNull String key) { + return commandManager.submitNewCommand( + SortReadOnly, + new String[] {key}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull String key, @NonNull SortBaseOptions sortBaseOptions) { + String[] arguments = ArrayUtils.addFirst(sortBaseOptions.toArgs(), key); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortStore(@NonNull String key, @NonNull String destination) { + return commandManager.submitNewCommand( + Sort, new String[] {key, STORE_COMMAND_STRING, destination}, this::handleLongResponse); + } + + @Override + public CompletableFuture sortStore( + @NonNull String key, @NonNull String destination, @NonNull SortBaseOptions sortBaseOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + String[] arguments = + ArrayUtils.addFirst(concatenateArrays(storeArguments, sortBaseOptions.toArgs()), key); + return commandManager.submitNewCommand(Sort, arguments, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index e090dcd2e4..421d9241b9 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; import static glide.utils.ArrayTransformUtils.castArray; import static glide.utils.ArrayTransformUtils.concatenateArrays; @@ -21,6 +22,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Move; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.Select; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Time; import glide.api.commands.ConnectionManagementCommands; @@ -30,6 +33,7 @@ import glide.api.models.Transaction; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortStandaloneOptions; import glide.api.models.configuration.RedisClientConfiguration; import glide.managers.CommandManager; import glide.managers.ConnectionManager; @@ -37,6 +41,7 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; /** * Async (non-blocking) client for Redis in Standalone mode. Use {@link #CreateClient} to request a @@ -203,4 +208,33 @@ public CompletableFuture move(@NonNull String key, long dbIndex) { return commandManager.submitNewCommand( Move, new String[] {key, Long.toString(dbIndex)}, this::handleBooleanResponse); } + + @Override + public CompletableFuture sort( + @NonNull String key, @NonNull SortStandaloneOptions sortStandaloneOptions) { + String[] arguments = ArrayUtils.addFirst(sortStandaloneOptions.toArgs(), key); + return commandManager.submitNewCommand( + Sort, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull String key, @NonNull SortStandaloneOptions sortStandaloneOptions) { + String[] arguments = ArrayUtils.addFirst(sortStandaloneOptions.toArgs(), key); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortStore( + @NonNull String key, + @NonNull String destination, + @NonNull SortStandaloneOptions sortStandaloneOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + String[] arguments = + ArrayUtils.addFirst(concatenateArrays(storeArguments, sortStandaloneOptions.toArgs()), key); + return commandManager.submitNewCommand(Sort, arguments, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java index 0889809b3f..c46515005b 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -4,6 +4,8 @@ import glide.api.models.Script; import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.ScriptOptions; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** @@ -542,4 +544,150 @@ CompletableFuture pexpireAt( * } */ CompletableFuture touch(String[] keys); + + /** + * 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(String, String)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assertArrayEquals(new String[] {"1", "2", "3"}, client.sort("mylist").get()); // List is sorted in ascending order
+     * }
+ */ + CompletableFuture sort(String key); + + /** + * 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(String, String, SortBaseOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortBaseOptions The {@link SortBaseOptions}. + * @return A Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * String[] payload = client.sort(
+     *      "mylist",
+     *      SortBaseOptions.builder()
+     *          .alpha(true)
+     *          .orderBy(DESC)
+     *          .limit(new SortOptions.Limit(0L, 3L))
+     *          .build())
+     *      .get();
+     * assertArrayEquals(new String[] {"a", "3", "2"}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sort(String key, SortBaseOptions sortBaseOptions); + + /** + * 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 Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @return A Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assertArrayEquals(new String[] {"1", "2", "3"}, client.sortReadOnly("mylist").get()); // List is sorted in ascending order
+     * }
+ */ + CompletableFuture sortReadOnly(String key); + + /** + * 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 Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortBaseOptions The {@link SortBaseOptions}. + * @return A Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * String[] payload = client.sortReadOnly(
+     *      "mylist",
+     *      SortBaseOptions.builder()
+     *          .alpha(true)
+     *          .orderBy(DESC)
+     *          .limit(new SortOptions.Limit(0L, 3L))
+     *          .build())
+     *      .get();
+     * assertArrayEquals(new String[] {"a", "3", "2"}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sortReadOnly(String key, SortBaseOptions sortBaseOptions); + + /** + * 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, 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(String)} and {@link + * #sortReadOnly(String)}. + * + * @apiNote When in cluster mode, key and destination 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. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assert client.sortStore("mylist", "destination").get() == 3;
+     * assertArrayEquals(
+     *    new String[] {"1", "2", "3"},
+     *    client.lrange("destination", 0, -1).get()); // Sorted list is stored in `destination`
+     * }
+ */ + CompletableFuture sortStore(String key, String destination); + + /** + * 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, 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(String, + * SortBaseOptions)} and {@link #sortReadOnly(String, SortBaseOptions)}. + * + * @apiNote When in cluster mode, key and destination 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 sortBaseOptions The {@link SortBaseOptions}. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * Long payload = client
+     *      .sortStore(
+     *          "mylist",
+     *          "destination",
+     *          SortBaseOptions.builder()
+     *              .alpha(true)
+     *              .orderBy(DESC)
+     *              .limit(new SortOptions.Limit(0L, 3L))
+     *              .build())
+     *      .get();
+     * assertEquals(3, payload);
+     * assertArrayEquals(
+     *      new String[] {"a", "3", "2"},
+     *      client.lrange("destination", 0, -1).get()); // Sorted list is stored in "destination"
+     * }
+ */ + CompletableFuture sortStore( + String key, String destination, SortBaseOptions sortBaseOptions); } diff --git a/java/client/src/main/java/glide/api/commands/GenericCommands.java b/java/client/src/main/java/glide/api/commands/GenericCommands.java index 77d74c7e7e..3e25cb0367 100644 --- a/java/client/src/main/java/glide/api/commands/GenericCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericCommands.java @@ -2,6 +2,8 @@ package glide.api.commands; import glide.api.models.Transaction; +import glide.api.models.commands.SortStandaloneOptions; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** @@ -72,4 +74,93 @@ public interface GenericCommands { * } */ CompletableFuture move(String key, long dbIndex); + + /** + * 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(String, String, + * SortStandaloneOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortStandaloneOptions The {@link SortStandaloneOptions}. + * @return A Array of sorted elements. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * String [] payload = client.sort(
+     *      "user_ids",
+     *      SortStandaloneOptions
+     *          .builder()
+     *              .byPattern("user:*->age")
+     *                  .getPatterns(new String[] {"user:*->name"})
+     *                      .build()).get();
+     * assertArrayEquals(new String[] {"Bob", "Alice"}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sort(String key, SortStandaloneOptions sortStandaloneOptions); + + /** + * 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 Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortStandaloneOptions The {@link SortStandaloneOptions}. + * @return A Array of sorted elements. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * String [] payload = client.sortReadOnly(
+     *      "user_ids",
+     *      SortStandaloneOptions
+     *          .builder()
+     *              .byPattern("user:*->age")
+     *                  .getPatterns(new String[] {"user:*->name"})
+     *                      .build()).get();
+     * assertArrayEquals(new String[] {"Bob", "Alice"}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sortReadOnly(String key, SortStandaloneOptions sortStandaloneOptions); + + /** + * 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, 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(String, + * SortStandaloneOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortStandaloneOptions The {@link SortStandaloneOptions}. + * @param destination The key where the sorted result will be stored. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * Long payload = client
+     *      .sortStore(
+     *          "user_ids",
+     *          "destination",
+     *          SortStandaloneOptions.builder()
+     *              .byPattern("user:*->age")
+     *              .getPatterns(new String[] {"user:*->name"})
+     *              .build())
+     *          .get();
+     * assertEquals(2, payload);
+     * assertArrayEquals(
+     *      new String[] {"Bob", "Alice"},
+     *      client.lrange("destination", 0, -1).get()); // The list of the names sorted by age is stored in `destination`
+     * }
+ */ + CompletableFuture sortStore( + String key, String destination, SortStandaloneOptions sortStandaloneOptions); } diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 0d7c361aa7..94a793e105 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -10,6 +10,7 @@ import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; import static glide.api.models.commands.RangeOptions.createZRangeArgs; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; import static glide.utils.ArrayTransformUtils.concatenateArrays; @@ -123,6 +124,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Set; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -184,6 +187,7 @@ import glide.api.models.commands.SetOptions; import glide.api.models.commands.SetOptions.ConditionalSet; import glide.api.models.commands.SetOptions.SetOptionsBuilder; +import glide.api.models.commands.SortBaseOptions; import glide.api.models.commands.WeightAggregateOptions; import glide.api.models.commands.WeightAggregateOptions.Aggregate; import glide.api.models.commands.WeightAggregateOptions.KeyArray; @@ -207,6 +211,7 @@ import glide.api.models.commands.stream.StreamAddOptions.StreamAddOptionsBuilder; import glide.api.models.commands.stream.StreamRange; import glide.api.models.commands.stream.StreamTrimOptions; +import glide.api.models.configuration.ReadFrom; import java.util.Arrays; import java.util.Map; import lombok.Getter; @@ -4072,6 +4077,115 @@ public T bitfieldReadOnly( return getThis(); } + /** + * 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(String, String)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @return Command Response - A Array of sorted elements. + */ + public T sort(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return getThis(); + } + + /** + * 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(String, String, SortBaseOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortBaseOptions The {@link SortBaseOptions}. + * @return Command Response - A Array of sorted elements. + */ + public T sort(@NonNull String key, @NonNull SortBaseOptions sortBaseOptions) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sortBaseOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return getThis(); + } + + /** + * 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 Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @return Command Response - A Array of sorted elements. + */ + public T sortReadOnly(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + protobufTransaction.addCommands(buildCommand(SortReadOnly, commandArgs)); + return getThis(); + } + + /** + * 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 Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortBaseOptions The {@link SortBaseOptions}. + * @return Command Response - A Array of sorted elements. + */ + public T sortReadOnly(@NonNull String key, @NonNull SortBaseOptions sortBaseOptions) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sortBaseOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(SortReadOnly, commandArgs)); + return getThis(); + } + + /** + * 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, 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(String)} and {@link + * #sortReadOnly(String)}. + * + * @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. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public T sortStore(@NonNull String key, @NonNull String destination) { + ArgsArray commandArgs = buildArgs(new String[] {key, STORE_COMMAND_STRING, destination}); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return getThis(); + } + + /** + * 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, 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(String, + * SortBaseOptions)} and {@link #sortReadOnly(String, SortBaseOptions)}. + * + * @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 sortBaseOptions The {@link SortBaseOptions}. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public T sortStore( + @NonNull String key, @NonNull String destination, @NonNull SortBaseOptions sortBaseOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + ArgsArray commandArgs = + buildArgs( + ArrayUtils.addFirst(ArrayUtils.addAll(storeArguments, sortBaseOptions.toArgs()), key)); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return getThis(); + } + /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { return buildCommand(requestType, buildArgs()); diff --git a/java/client/src/main/java/glide/api/models/Transaction.java b/java/client/src/main/java/glide/api/models/Transaction.java index b2230974c7..9223d0f4c8 100644 --- a/java/client/src/main/java/glide/api/models/Transaction.java +++ b/java/client/src/main/java/glide/api/models/Transaction.java @@ -1,10 +1,17 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static redis_request.RedisRequestOuterClass.RequestType.Move; import static redis_request.RedisRequestOuterClass.RequestType.Select; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; +import glide.api.models.commands.SortStandaloneOptions; +import glide.api.models.configuration.ReadFrom; import lombok.AllArgsConstructor; +import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; import redis_request.RedisRequestOuterClass.Command.ArgsArray; /** @@ -64,4 +71,67 @@ public Transaction move(String key, long dbIndex) { protobufTransaction.addCommands(buildCommand(Move, commandArgs)); return this; } + + /** + * 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(String, String, + * SortStandaloneOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortStandaloneOptions The {@link SortStandaloneOptions}. + * @return Command Response - A Array of sorted elements. + */ + public Transaction sort( + @NonNull String key, @NonNull SortStandaloneOptions sortStandaloneOptions) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sortStandaloneOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return this; + } + + /** + * 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 Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortStandaloneOptions The {@link SortStandaloneOptions}. + * @return Command Response - A Array of sorted elements. + */ + public Transaction sortReadOnly( + @NonNull String key, @NonNull SortStandaloneOptions sortStandaloneOptions) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sortStandaloneOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(SortReadOnly, commandArgs)); + return this; + } + + /** + * 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, 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(String, + * SortStandaloneOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortStandaloneOptions The {@link SortStandaloneOptions}. + * @param destination The key where the sorted result will be stored. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public Transaction sortStore( + @NonNull String key, + @NonNull String destination, + @NonNull SortStandaloneOptions sortStandaloneOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + ArgsArray arguments = + buildArgs( + ArrayUtils.addFirst( + ArrayUtils.addAll(storeArguments, sortStandaloneOptions.toArgs()), key)); + protobufTransaction.addCommands(buildCommand(Sort, arguments)); + return this; + } } diff --git a/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java b/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java new file mode 100644 index 0000000000..62702d1545 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java @@ -0,0 +1,13 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericBaseCommands; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericBaseCommands#sort(String, SortBaseOptions)}, {@link + * GenericBaseCommands#sortReadOnly(String, SortBaseOptions)}, and {@link + * GenericBaseCommands#sortStore(String, String, SortBaseOptions)} + */ +@SuperBuilder +public class SortBaseOptions extends SortOptions {} diff --git a/java/client/src/main/java/glide/api/models/commands/SortOptions.java b/java/client/src/main/java/glide/api/models/commands/SortOptions.java new file mode 100644 index 0000000000..9ebdc0d272 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortOptions.java @@ -0,0 +1,108 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericBaseCommands; +import glide.api.commands.GenericCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericBaseCommands#sort(String, SortBaseOptions)}, {@link + * GenericBaseCommands#sortReadOnly(String, SortBaseOptions)}, {@link + * GenericBaseCommands#sortStore(String, String, SortBaseOptions)}, {@link + * GenericCommands#sort(String, SortStandaloneOptions)}, {@link GenericCommands#sortReadOnly(String, + * SortStandaloneOptions)}, and {@link GenericCommands#sortStore(String, String, + * SortStandaloneOptions)} + * + * @see redis.io and redis.io + */ +@SuperBuilder +public abstract class SortOptions { + /** + * LIMIT subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String LIMIT_COMMAND_STRING = "LIMIT"; + + /** + * ALPHA subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String ALPHA_COMMAND_STRING = "ALPHA"; + + /** STORE subcommand string to include in the SORT command. */ + public static final String STORE_COMMAND_STRING = "STORE"; + + /** + * Limiting the range of the query by setting offset and result count. See `Limit` class for more + * information. + */ + private final Limit limit; + + /** Options for sorting order of elements. */ + private final OrderBy orderBy; + + /** + * 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. + */ + private final boolean alpha; + + /** + * 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`). + */ + @RequiredArgsConstructor + public static final class Limit { + /** The starting position of the range, zero based. */ + private final long offset; + + /** + * The maximum number of elements to include in the range. A negative count returns all elements + * from the offset. + */ + private final long count; + } + + /** + * Specifies the order to sort the elements. Can be ASC (ascending) or DESC + * (descending). + */ + @RequiredArgsConstructor + public enum OrderBy { + ASC, + DESC + } + + /** + * Creates the arguments to be used in SORT and SORT_RO commands. + * + * @return a String array that holds the sub commands and their arguments. + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + + if (limit != null) { + optionArgs.addAll( + List.of( + LIMIT_COMMAND_STRING, + Long.toString(this.limit.offset), + Long.toString(this.limit.count))); + } + + if (orderBy != null) { + optionArgs.add(this.orderBy.toString()); + } + + if (alpha) { + optionArgs.add(ALPHA_COMMAND_STRING); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/SortStandaloneOptions.java b/java/client/src/main/java/glide/api/models/commands/SortStandaloneOptions.java new file mode 100644 index 0000000000..d0d53f9263 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortStandaloneOptions.java @@ -0,0 +1,75 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericCommands#sort(String, SortStandaloneOptions)}, {@link + * GenericCommands#sortReadOnly(String, SortStandaloneOptions)}, and {@link + * GenericCommands#sortStore(String, String, SortStandaloneOptions)} + * + * @see redis.io and redis.io + */ +@SuperBuilder +public class SortStandaloneOptions extends SortOptions { + /** + * BY subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String BY_COMMAND_STRING = "BY"; + + /** + * GET subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String GET_COMMAND_STRING = "GET"; + + /** + * 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. + */ + private final String byPattern; + + /** + * 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_<element> 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. + */ + private final String[] getPatterns; + + /** + * Creates the arguments to be used in SORT and SORT_RO commands. + * + * @return a String array that holds the sub commands and their arguments. + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(List.of(super.toArgs())); + + if (byPattern != null) { + optionArgs.addAll(List.of(BY_COMMAND_STRING, byPattern)); + } + + if (getPatterns != null) { + for (int i = 0; i < getPatterns.length; i++) { + optionArgs.addAll(List.of(GET_COMMAND_STRING, getPatterns[i])); + } + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 285cf142c4..aa27c375c3 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -15,6 +15,11 @@ import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static glide.api.models.commands.SortOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.OrderBy.DESC; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortStandaloneOptions.BY_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow.BitOverflowControl.SAT; import static glide.api.models.commands.bitmap.BitFieldOptions.GET_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.INCRBY_COMMAND_STRING; @@ -151,6 +156,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Select; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -207,6 +214,8 @@ import glide.api.models.commands.ScriptOptions; import glide.api.models.commands.SetOptions; import glide.api.models.commands.SetOptions.Expiry; +import glide.api.models.commands.SortOptions; +import glide.api.models.commands.SortStandaloneOptions; import glide.api.models.commands.WeightAggregateOptions.Aggregate; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; @@ -5583,4 +5592,256 @@ public void move_returns_success() { assertEquals(testResponse, response); assertEquals(value, payload); } + + @SneakyThrows + @Test + public void sort_with_standalone_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] {key, BY_COMMAND_STRING, byPattern, GET_COMMAND_STRING, getPattern}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortStandaloneOptions.builder() + .getPatterns(new String[] {getPattern}) + .byPattern(byPattern) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sort_with_standalone_and_sort_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + byPattern, + GET_COMMAND_STRING, + getPattern + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortStandaloneOptions.builder() + .alpha(true) + .limit(new SortOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPatterns(new String[] {getPattern}) + .byPattern(byPattern) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_standalone_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] {key, BY_COMMAND_STRING, byPattern, GET_COMMAND_STRING, getPattern}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, + SortStandaloneOptions.builder() + .getPatterns(new String[] {getPattern}) + .byPattern(byPattern) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_standalone_and_sort_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + byPattern, + GET_COMMAND_STRING, + getPattern + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, + SortStandaloneOptions.builder() + .alpha(true) + .limit(new SortOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPatterns(new String[] {getPattern}) + .byPattern(byPattern) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore__standalone_options_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] { + key, + STORE_COMMAND_STRING, + destKey, + BY_COMMAND_STRING, + byPattern, + GET_COMMAND_STRING, + getPattern + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortStandaloneOptions.builder() + .getPatterns(new String[] {getPattern}) + .byPattern(byPattern) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_with_standalone_and_sort_options_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + Long limitOffset = 0L; + Long limitCount = 2L; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] { + key, + STORE_COMMAND_STRING, + destKey, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + byPattern, + GET_COMMAND_STRING, + getPattern + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortStandaloneOptions.builder() + .alpha(true) + .limit(new SortOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPatterns(new String[] {getPattern}) + .byPattern(byPattern) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } } diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 0af49287f8..87f746b797 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -4,6 +4,10 @@ import static glide.api.BaseClient.OK; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.models.commands.FlushMode.SYNC; +import static glide.api.models.commands.SortOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.OrderBy.DESC; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; @@ -28,11 +32,15 @@ import static redis_request.RedisRequestOuterClass.RequestType.LastSave; import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.Ping; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Time; import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortOptions.Limit; import glide.api.models.commands.function.FunctionLoadOptions; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -1169,4 +1177,195 @@ public void functionLoad_with_replace_with_route_returns_success() { assertEquals(testResponse, response); assertEquals(value, payload); } + + @SneakyThrows + @Test + public void sort_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sort(key); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sort_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortBaseOptions.builder() + .alpha(true) + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sortReadOnly(key); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, + SortBaseOptions.builder() + .alpha(true) + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(Sort), eq(new String[] {key, STORE_COMMAND_STRING, destKey}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sortStore(key, destKey); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_with_options_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + STORE_COMMAND_STRING, + destKey, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortBaseOptions.builder() + .alpha(true) + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } } diff --git a/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java b/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java index c64c0992c1..318b5fde6f 100644 --- a/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java +++ b/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java @@ -2,10 +2,20 @@ package glide.api.models; import static glide.api.models.TransactionTests.buildArgs; +import static glide.api.models.commands.SortOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.Limit; +import static glide.api.models.commands.SortOptions.OrderBy.DESC; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortStandaloneOptions.BY_COMMAND_STRING; +import static glide.api.models.commands.SortStandaloneOptions.GET_COMMAND_STRING; import static org.junit.jupiter.api.Assertions.assertEquals; import static redis_request.RedisRequestOuterClass.RequestType.Move; import static redis_request.RedisRequestOuterClass.RequestType.Select; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; +import glide.api.models.commands.SortStandaloneOptions; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.tuple.Pair; @@ -24,6 +34,139 @@ public void standalone_transaction_commands() { transaction.move("testKey", 2L); results.add(Pair.of(Move, buildArgs("testKey", "2"))); + transaction.sort( + "key1", + SortStandaloneOptions.builder() + .byPattern("byPattern") + .getPatterns(new String[] {"getPattern1", "getPattern2"}) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sort( + "key1", + SortStandaloneOptions.builder() + .orderBy(DESC) + .alpha(true) + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(new String[] {"getPattern1", "getPattern2"}) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortReadOnly( + "key1", + SortStandaloneOptions.builder() + .byPattern("byPattern") + .getPatterns(new String[] {"getPattern1", "getPattern2"}) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortReadOnly( + "key1", + SortStandaloneOptions.builder() + .orderBy(DESC) + .alpha(true) + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(new String[] {"getPattern1", "getPattern2"}) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortStore( + "key1", + "key2", + SortStandaloneOptions.builder() + .byPattern("byPattern") + .getPatterns(new String[] {"getPattern1", "getPattern2"}) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + STORE_COMMAND_STRING, + "key2", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortStore( + "key1", + "key2", + SortStandaloneOptions.builder() + .orderBy(DESC) + .alpha(true) + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(new String[] {"getPattern1", "getPattern2"}) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + STORE_COMMAND_STRING, + "key2", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index e1c7fdb269..4c9b85b8a3 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -17,6 +17,10 @@ import static glide.api.models.commands.ScoreFilter.MAX; import static glide.api.models.commands.ScoreFilter.MIN; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static glide.api.models.commands.SortOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.OrderBy.ASC; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.WeightAggregateOptions.AGGREGATE_REDIS_API; import static glide.api.models.commands.WeightAggregateOptions.WEIGHTS_REDIS_API; import static glide.api.models.commands.ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT; @@ -133,6 +137,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Set; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -184,6 +190,8 @@ import glide.api.models.commands.RangeOptions.RangeByScore; import glide.api.models.commands.RangeOptions.ScoreBoundary; import glide.api.models.commands.SetOptions; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.commands.WeightAggregateOptions.Aggregate; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; @@ -928,6 +936,57 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.sintercard(new String[] {"key1", "key2"}, 1); results.add(Pair.of(SInterCard, buildArgs("2", "key1", "key2", "LIMIT", "1"))); + transaction.sort("key1"); + results.add(Pair.of(Sort, buildArgs("key1"))); + transaction.sort( + "key1", + SortBaseOptions.builder() + .orderBy(ASC) + .alpha(true) + .limit(new SortOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", LIMIT_COMMAND_STRING, "0", "1", ASC.toString(), ALPHA_COMMAND_STRING))); + transaction.sortReadOnly("key1"); + results.add(Pair.of(SortReadOnly, buildArgs("key1"))); + transaction.sortReadOnly( + "key1", + SortBaseOptions.builder() + .orderBy(ASC) + .alpha(true) + .limit(new SortOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", LIMIT_COMMAND_STRING, "0", "1", ASC.toString(), ALPHA_COMMAND_STRING))); + transaction.sortStore("key1", "key2"); + results.add(Pair.of(Sort, buildArgs("key1", STORE_COMMAND_STRING, "key2"))); + transaction.sortStore( + "key1", + "key2", + SortBaseOptions.builder() + .orderBy(ASC) + .alpha(true) + .limit(new SortOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + STORE_COMMAND_STRING, + "key2", + LIMIT_COMMAND_STRING, + "0", + "1", + ASC.toString(), + ALPHA_COMMAND_STRING))); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 2064cc8087..9d312ed031 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -17,6 +17,7 @@ import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.Expiry.Milliseconds; +import static glide.api.models.commands.SortOptions.OrderBy.DESC; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -44,6 +45,8 @@ import glide.api.models.commands.RangeOptions.ScoreBoundary; import glide.api.models.commands.ScriptOptions; import glide.api.models.commands.SetOptions; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.commands.WeightAggregateOptions.Aggregate; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; @@ -4900,4 +4903,96 @@ public void sintercard(BaseClient client) { assertThrows(ExecutionException.class, () -> client.sintercard(badArr).get()); assertInstanceOf(RequestException.class, executionException.getCause()); } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sort(BaseClient client) { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String[] key1LpushArgs = {"2", "1", "4", "3"}; + String[] key1AscendingList = {"1", "2", "3", "4"}; + String[] key1DescendingList = {"4", "3", "2", "1"}; + String[] key2LpushArgs = {"2", "1", "a", "x", "c", "4", "3"}; + String[] key2DescendingList = {"x", "c", "a", "4", "3", "2", "1"}; + String[] key2DescendingListSubset = Arrays.copyOfRange(key2DescendingList, 0, 4); + + assertArrayEquals(new String[0], client.sort(key3).get()); + assertEquals(4, client.lpush(key1, key1LpushArgs).get()); + assertArrayEquals( + new String[0], + client + .sort(key1, SortBaseOptions.builder().limit(new SortOptions.Limit(0L, 0L)).build()) + .get()); + assertArrayEquals(key1AscendingList, client.sort(key1).get()); + assertArrayEquals( + key1DescendingList, + client.sort(key1, SortBaseOptions.builder().orderBy(DESC).build()).get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + client + .sort(key1, SortBaseOptions.builder().limit(new SortOptions.Limit(0L, 2L)).build()) + .get()); + assertEquals(7, client.lpush(key2, key2LpushArgs).get()); + assertArrayEquals( + key2DescendingListSubset, + client + .sort( + key2, + SortBaseOptions.builder() + .alpha(true) + .orderBy(DESC) + .limit(new SortOptions.Limit(0L, 4L)) + .build()) + .get()); + + // SORT_R0 + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals(new String[0], client.sortReadOnly(key3).get()); + assertArrayEquals(key1AscendingList, client.sortReadOnly(key1).get()); + assertArrayEquals( + key1DescendingList, + client.sortReadOnly(key1, SortBaseOptions.builder().orderBy(DESC).build()).get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + client + .sortReadOnly( + key1, SortBaseOptions.builder().limit(new SortOptions.Limit(0L, 2L)).build()) + .get()); + assertArrayEquals( + key2DescendingListSubset, + client + .sortReadOnly( + key2, + SortBaseOptions.builder() + .alpha(true) + .orderBy(DESC) + .limit(new SortOptions.Limit(0L, 4L)) + .build()) + .get()); + } + // SORT with STORE + assertEquals(4, client.sortStore(key1, key3).get()); + assertArrayEquals(key1AscendingList, client.lrange(key3, 0, -1).get()); + assertEquals( + 4, + client + .sortStore( + key2, + key3, + SortBaseOptions.builder() + .alpha(true) + .orderBy(DESC) + .limit(new SortOptions.Limit(0L, 4L)) + .build()) + .get()); + assertArrayEquals(key2DescendingListSubset, client.lrange(key3, 0, -1).get()); + + // Exceptions + // SORT with strings require ALPHA + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sort(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 6acdaf5215..223afebe7e 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -7,6 +7,7 @@ import static glide.api.models.commands.LInsertOptions.InsertPosition.AFTER; import static glide.api.models.commands.ScoreFilter.MAX; import static glide.api.models.commands.ScoreFilter.MIN; +import static glide.api.models.commands.SortOptions.OrderBy.DESC; import static glide.utils.ArrayTransformUtils.concatenateArrays; import glide.api.models.BaseTransaction; @@ -18,6 +19,7 @@ import glide.api.models.commands.RangeOptions.RangeByIndex; import glide.api.models.commands.RangeOptions.ScoreBoundary; import glide.api.models.commands.SetOptions; +import glide.api.models.commands.SortBaseOptions; import glide.api.models.commands.WeightAggregateOptions.Aggregate; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; @@ -96,6 +98,10 @@ public static Stream getPrimaryNodeTransactionBuilders() { private static Object[] genericCommands(BaseTransaction transaction) { String genericKey1 = "{GenericKey}-1-" + UUID.randomUUID(); String genericKey2 = "{GenericKey}-2-" + UUID.randomUUID(); + String genericKey3 = "{GenericKey}-3-" + UUID.randomUUID(); + String genericKey4 = "{GenericKey}-4-" + UUID.randomUUID(); + String[] ascendingList = new String[] {"1", "2", "3"}; + String[] descendingList = new String[] {"3", "2", "1"}; transaction .set(genericKey1, value1) @@ -117,7 +123,14 @@ private static Object[] genericCommands(BaseTransaction transaction) { .expireAt(genericKey1, 42) // expire (delete) key immediately .pexpire(genericKey1, 42) .pexpireAt(genericKey1, 42) - .ttl(genericKey2); + .ttl(genericKey2) + .lpush(genericKey3, new String[] {"3", "1", "2"}) + .sort(genericKey3) + .sort(genericKey3, SortBaseOptions.builder().orderBy(DESC).build()) + .sortStore(genericKey3, genericKey4) + .lrange(genericKey4, 0, -1) + .sortStore(genericKey3, genericKey4, SortBaseOptions.builder().orderBy(DESC).build()) + .lrange(genericKey4, 0, -1); if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { transaction @@ -127,7 +140,9 @@ private static Object[] genericCommands(BaseTransaction transaction) { .pexpire(genericKey1, 42, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT) .pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) .expiretime(genericKey1) - .pexpiretime(genericKey1); + .pexpiretime(genericKey1) + .sortReadOnly(genericKey3) + .sortReadOnly(genericKey3, SortBaseOptions.builder().orderBy(DESC).build()); } var expectedResults = @@ -152,6 +167,13 @@ private static Object[] genericCommands(BaseTransaction transaction) { false, // pexpire(genericKey1, 42) false, // pexpireAt(genericKey1, 42) -2L, // ttl(genericKey2) + 3L, // lpush(genericKey3, new String[] {"3", "1", "2"}) + ascendingList, // sort(genericKey3) + descendingList, // sort(genericKey3, SortBaseOptions.builder().orderBy(DESC).build()) + 3L, // sortStore(genericKey3, genericKey4) + ascendingList, // lrange(genericKey4, 0, -1) + 3L, // sortStore(genericKey3, genericKey4, DESC)) + descendingList, // lrange(genericKey4, 0, -1) }; if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { @@ -165,6 +187,8 @@ private static Object[] genericCommands(BaseTransaction transaction) { false, // pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) -2L, // expiretime(genericKey1) -2L, // pexpiretime(genericKey1) + ascendingList, // sortReadOnly(genericKey3) + descendingList, // sortReadOnly(genericKey3, DESC) }); } return expectedResults; diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 19fbf1e107..61b580cdfa 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -12,9 +12,13 @@ import static glide.api.models.commands.InfoOptions.Section.MEMORY; import static glide.api.models.commands.InfoOptions.Section.SERVER; import static glide.api.models.commands.InfoOptions.Section.STATS; +import static glide.api.models.commands.SortOptions.Limit; +import static glide.api.models.commands.SortOptions.OrderBy.ASC; +import static glide.api.models.commands.SortOptions.OrderBy.DESC; import static glide.cluster.CommandTests.DEFAULT_INFO_SECTIONS; import static glide.cluster.CommandTests.EVERYTHING_INFO_SECTIONS; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; @@ -25,6 +29,7 @@ import glide.api.RedisClient; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortStandaloneOptions; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClientConfiguration; import glide.api.models.exceptions.RequestException; @@ -394,4 +399,208 @@ public void functionLoad() { assertEquals(libName, regularClient.functionLoad(newCode, true).get()); // TODO test with FCALL } + + @Test + @SneakyThrows + public void sort() { + String setKey1 = "setKey1"; + String setKey2 = "setKey2"; + String setKey3 = "setKey3"; + String setKey4 = "setKey4"; + String setKey5 = "setKey5"; + String[] setKeys = new String[] {setKey1, setKey2, setKey3, setKey4, setKey5}; + String listKey = "listKey"; + String storeKey = "storeKey"; + String nameField = "name"; + String ageField = "age"; + String[] names = new String[] {"Alice", "Bob", "Charlie", "Dave", "Eve"}; + String[] namesSortedByAge = new String[] {"Dave", "Bob", "Alice", "Charlie", "Eve"}; + String[] ages = new String[] {"30", "25", "35", "20", "40"}; + String[] userIDs = new String[] {"3", "1", "5", "4", "2"}; + String namePattern = "setKey*->name"; + String agePattern = "setKey*->age"; + String missingListKey = "100000"; + + for (int i = 0; i < setKeys.length; i++) { + assertEquals( + 2, regularClient.hset(setKeys[i], Map.of(nameField, names[i], ageField, ages[i])).get()); + } + + assertEquals(5, regularClient.rpush(listKey, userIDs).get()); + assertArrayEquals( + new String[] {"Alice", "Bob"}, + regularClient + .sort( + listKey, + SortStandaloneOptions.builder() + .limit(new Limit(0L, 2L)) + .getPatterns(new String[] {namePattern}) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "Dave"}, + regularClient + .sort( + listKey, + SortStandaloneOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPatterns(new String[] {namePattern}) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "40", "Charlie", "35"}, + regularClient + .sort( + listKey, + SortStandaloneOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(new String[] {namePattern, agePattern}) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient + .sort(listKey, SortStandaloneOptions.builder().byPattern("noSort").build()) + .get()); + + // Non-existent key in the GET pattern results in nulls + assertArrayEquals( + new String[] {null, null, null, null, null}, + regularClient + .sort( + listKey, + SortStandaloneOptions.builder() + .alpha(true) + .getPatterns(new String[] {"missing"}) + .build()) + .get()); + + // Missing key in the set + assertEquals(6, regularClient.lpush(listKey, new String[] {missingListKey}).get()); + assertArrayEquals( + new String[] {null, "Dave", "Bob", "Alice", "Charlie", "Eve"}, + regularClient + .sort( + listKey, + SortStandaloneOptions.builder() + .byPattern(agePattern) + .getPatterns(new String[] {namePattern}) + .build()) + .get()); + assertEquals(missingListKey, regularClient.lpop(listKey).get()); + + // SORT_RO + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals( + new String[] {"Alice", "Bob"}, + regularClient + .sortReadOnly( + listKey, + SortStandaloneOptions.builder() + .limit(new Limit(0L, 2L)) + .getPatterns(new String[] {namePattern}) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "Dave"}, + regularClient + .sortReadOnly( + listKey, + SortStandaloneOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPatterns(new String[] {namePattern}) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "40", "Charlie", "35"}, + regularClient + .sortReadOnly( + listKey, + SortStandaloneOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(new String[] {namePattern, agePattern}) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient + .sortReadOnly(listKey, SortStandaloneOptions.builder().byPattern("noSort").build()) + .get()); + + // Non-existent key in the GET pattern results in nulls + assertArrayEquals( + new String[] {null, null, null, null, null}, + regularClient + .sortReadOnly( + listKey, + SortStandaloneOptions.builder() + .alpha(true) + .getPatterns(new String[] {"missing"}) + .build()) + .get()); + + assertArrayEquals( + namesSortedByAge, + regularClient + .sortReadOnly( + listKey, + SortStandaloneOptions.builder() + .byPattern(agePattern) + .getPatterns(new String[] {namePattern}) + .build()) + .get()); + + // Missing key in the set + assertEquals(6, regularClient.lpush(listKey, new String[] {missingListKey}).get()); + assertArrayEquals( + new String[] {null, "Dave", "Bob", "Alice", "Charlie", "Eve"}, + regularClient + .sortReadOnly( + listKey, + SortStandaloneOptions.builder() + .byPattern(agePattern) + .getPatterns(new String[] {namePattern}) + .build()) + .get()); + assertEquals(missingListKey, regularClient.lpop(listKey).get()); + } + + // SORT with STORE + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortStandaloneOptions.builder() + .limit(new Limit(0L, -1L)) + .orderBy(ASC) + .byPattern(agePattern) + .getPatterns(new String[] {namePattern}) + .build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey, 0, -1).get()); + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortStandaloneOptions.builder() + .byPattern(agePattern) + .getPatterns(new String[] {namePattern}) + .build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey, 0, -1).get()); + } } diff --git a/java/integTest/src/test/java/glide/standalone/TransactionTests.java b/java/integTest/src/test/java/glide/standalone/TransactionTests.java index 3feb6a71e0..b14cacbfd1 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -4,6 +4,7 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestUtilities.assertDeepEquals; import static glide.api.BaseClient.OK; +import static glide.api.models.commands.SortOptions.OrderBy.DESC; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -16,6 +17,7 @@ import glide.api.RedisClient; import glide.api.models.Transaction; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortStandaloneOptions; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClientConfiguration; import java.time.Instant; @@ -227,4 +229,90 @@ public void WATCH_transaction_failure_returns_null() { assertEquals(OK, client.set("key", "foo").get()); assertNull(client.exec(transaction).get()); } + + @Test + @SneakyThrows + public void sort_and_sortReadOnly() { + Transaction transaction1 = new Transaction(); + Transaction transaction2 = new Transaction(); + String genericKey1 = "{GenericKey}-1-" + UUID.randomUUID(); + String genericKey2 = "{GenericKey}-2-" + UUID.randomUUID(); + String[] ascendingListByAge = new String[] {"Bob", "Alice"}; + String[] descendingListByAge = new String[] {"Alice", "Bob"}; + + transaction1 + .hset("user:1", Map.of("name", "Alice", "age", "30")) + .hset("user:2", Map.of("name", "Bob", "age", "25")) + .lpush(genericKey1, new String[] {"2", "1"}) + .sort( + genericKey1, + SortStandaloneOptions.builder() + .byPattern("user:*->age") + .getPatterns(new String[] {"user:*->name"}) + .build()) + .sort( + genericKey1, + SortStandaloneOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPatterns(new String[] {"user:*->name"}) + .build()) + .sortStore( + genericKey1, + genericKey2, + SortStandaloneOptions.builder() + .byPattern("user:*->age") + .getPatterns(new String[] {"user:*->name"}) + .build()) + .lrange(genericKey2, 0, -1) + .sortStore( + genericKey1, + genericKey2, + SortStandaloneOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPatterns(new String[] {"user:*->name"}) + .build()) + .lrange(genericKey2, 0, -1); + + var expectedResults = + new Object[] { + 2L, // hset("user:1", Map.of("name", "Alice", "age", "30")) + 2L, // hset("user:2", Map.of("name", "Bob", "age", "25")) + 2L, // lpush(genericKey1, new String[] {"2", "1"}) + ascendingListByAge, // sort(genericKey1, SortStandaloneOptions) + descendingListByAge, // sort(genericKey1, SortStandaloneOptions) + 2L, // sortStore(genericKey1, genericKey2, SortStandaloneOptions) + ascendingListByAge, // lrange(genericKey4, 0, -1) + 2L, // sortStore(genericKey1, genericKey2, SortStandaloneOptions) + descendingListByAge, // lrange(genericKey2, 0, -1) + }; + + assertArrayEquals(expectedResults, client.exec(transaction1).get()); + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction2 + .sortReadOnly( + genericKey1, + SortStandaloneOptions.builder() + .byPattern("user:*->age") + .getPatterns(new String[] {"user:*->name"}) + .build()) + .sortReadOnly( + genericKey1, + SortStandaloneOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPatterns(new String[] {"user:*->name"}) + .build()); + + expectedResults = + new Object[] { + ascendingListByAge, // sortReadOnly(genericKey1, SortStandaloneOptions) + descendingListByAge, // sortReadOnly(genericKey1, SortStandaloneOptions) + }; + + assertArrayEquals(expectedResults, client.exec(transaction2).get()); + } + } }