From 2438d8b5b5e39985b0bc98a33a778c05709d8419 Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Sat, 22 Jun 2024 00:53:20 +0000 Subject: [PATCH] Java: Add `SORT` and `SORT_RO` commands (#1611) * Java: Add `SORT` and `SORT_RO` commands (#363) Co-authored-by: Yury-Fridlyand --- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + .../src/main/java/glide/api/BaseClient.java | 26 +++ .../src/main/java/glide/api/RedisClient.java | 31 +++ .../java/glide/api/RedisClusterClient.java | 34 +++ .../api/commands/GenericBaseCommands.java | 60 ++++++ .../api/commands/GenericClusterCommands.java | 72 +++++++ .../glide/api/commands/GenericCommands.java | 72 +++++++ .../glide/api/models/BaseTransaction.java | 54 +++++ .../glide/api/models/ClusterTransaction.java | 73 +++++++ .../java/glide/api/models/Transaction.java | 60 ++++++ .../api/models/commands/SortBaseOptions.java | 109 ++++++++++ .../models/commands/SortClusterOptions.java | 13 ++ .../api/models/commands/SortOptions.java | 77 +++++++ .../test/java/glide/api/RedisClientTest.java | 136 ++++++++++++ .../glide/api/RedisClusterClientTest.java | 199 ++++++++++++++++++ .../api/models/ClusterTransactionTests.java | 94 +++++++++ .../models/StandaloneTransactionTests.java | 143 +++++++++++++ .../glide/api/models/TransactionTests.java | 10 + .../test/java/glide/SharedCommandTests.java | 33 +++ .../java/glide/TransactionTestUtilities.java | 16 +- .../cluster/ClusterTransactionTests.java | 39 ++++ .../test/java/glide/cluster/CommandTests.java | 96 ++++++++- .../java/glide/standalone/CommandTests.java | 180 ++++++++++++++++ .../glide/standalone/TransactionTests.java | 79 +++++++ 25 files changed, 1707 insertions(+), 3 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/SortClusterOptions.java create mode 100644 java/client/src/main/java/glide/api/models/commands/SortOptions.java create mode 100644 java/client/src/test/java/glide/api/models/ClusterTransactionTests.java diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index a7fd57ab1b..12341ea919 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -233,6 +233,7 @@ enum RequestType { GetEx = 192; Dump = 193; Restore = 194; + SortReadOnly = 195; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 3e3e917ce6..526c14a2f4 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -203,6 +203,7 @@ pub enum RequestType { GetEx = 192, Dump = 193, Restore = 194, + SortReadOnly = 195, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -409,6 +410,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::GetEx => RequestType::GetEx, ProtobufRequestType::Dump => RequestType::Dump, ProtobufRequestType::Restore => RequestType::Restore, + ProtobufRequestType::SortReadOnly => RequestType::SortReadOnly, } } } @@ -613,6 +615,7 @@ impl RequestType { RequestType::GetEx => Some(cmd("GETEX")), RequestType::Dump => Some(cmd("DUMP")), RequestType::Restore => Some(cmd("RESTORE")), + 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 4b89a72ff6..7167ba4bff 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -2,6 +2,8 @@ package glide.api; import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +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; @@ -118,6 +120,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; @@ -2080,4 +2084,26 @@ public CompletableFuture restore( GlideString[] arguments = restoreOptions.toArgs(key, ttl, value); return commandManager.submitNewCommand(Restore, arguments, this::handleStringResponse); } + + @Override + public CompletableFuture sort(@NonNull String key) { + return commandManager.submitNewCommand( + Sort, + new String[] {key}, + 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 sortStore(@NonNull String key, @NonNull String destination) { + return commandManager.submitNewCommand( + Sort, new String[] {key, STORE_COMMAND_STRING, destination}, 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 5722a071f6..3a748cc8de 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -1,6 +1,8 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; @@ -32,6 +34,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RandomKey; 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 static redis_request.RedisRequestOuterClass.RequestType.UnWatch; @@ -43,6 +47,7 @@ import glide.api.models.Transaction; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.configuration.RedisClientConfiguration; import glide.managers.CommandManager; import glide.managers.ConnectionManager; @@ -324,4 +329,30 @@ public CompletableFuture randomKey() { return commandManager.submitNewCommand( RandomKey, new String[0], this::handleStringOrNullResponse); } + + @Override + public CompletableFuture sort(@NonNull String key, @NonNull SortOptions sortOptions) { + String[] arguments = ArrayUtils.addFirst(sortOptions.toArgs(), key); + return commandManager.submitNewCommand( + Sort, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull String key, @NonNull SortOptions sortOptions) { + String[] arguments = ArrayUtils.addFirst(sortOptions.toArgs(), key); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortStore( + @NonNull String key, @NonNull String destination, @NonNull SortOptions sortOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + String[] arguments = + concatenateArrays(new String[] {key}, sortOptions.toArgs(), storeArguments); + return commandManager.submitNewCommand(Sort, arguments, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index b20f7b4202..0ac4374d87 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -2,6 +2,7 @@ package glide.api; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; @@ -33,6 +34,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RandomKey; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Time; import static redis_request.RedisRequestOuterClass.RequestType.UnWatch; @@ -45,6 +48,7 @@ import glide.api.models.ClusterValue; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -56,6 +60,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; import response.ResponseOuterClass.Response; /** @@ -701,4 +706,33 @@ public CompletableFuture randomKey() { return commandManager.submitNewCommand( RandomKey, new String[0], this::handleStringOrNullResponse); } + + @Override + public CompletableFuture sort( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + String[] arguments = ArrayUtils.addFirst(sortClusterOptions.toArgs(), key); + return commandManager.submitNewCommand( + Sort, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + String[] arguments = ArrayUtils.addFirst(sortClusterOptions.toArgs(), key); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortStore( + @NonNull String key, + @NonNull String destination, + @NonNull SortClusterOptions sortClusterOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + String[] arguments = + concatenateArrays(new String[] {key}, sortClusterOptions.toArgs(), storeArguments); + 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 c778d28f93..6c39ed8660 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -6,6 +6,7 @@ import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.RestoreOptions; import glide.api.models.commands.ScriptOptions; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** @@ -652,4 +653,63 @@ CompletableFuture pexpireAt( */ CompletableFuture restore( GlideString key, long ttl, byte[] value, RestoreOptions restoreOptions); + + /** + * 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 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 An 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 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)} or {@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); } diff --git a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java index a76290b3b3..74e287974f 100644 --- a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java @@ -4,6 +4,8 @@ import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; import glide.api.models.Transaction; +import glide.api.models.commands.SortClusterOptions; +import glide.api.models.configuration.ReadFrom; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import java.util.concurrent.CompletableFuture; @@ -148,4 +150,74 @@ public interface GenericClusterCommands { * } */ CompletableFuture randomKey(); + + /** + * 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, SortClusterOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * String[] payload = client.sort("mylist", SortClusterOptions.builder().alpha()
+     *          .orderBy(DESC).limit(new SortBaseOptions.Limit(0L, 3L)).build()).get();
+     * assertArrayEquals(new String[] {"a", "3", "2"}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sort(String key, SortClusterOptions sortClusterOptions); + + /** + * 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 sortClusterOptions The {@link SortClusterOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * String[] payload = client.sortReadOnly("mylist", SortClusterOptions.builder().alpha()
+     *          .orderBy(DESC).limit(new SortBaseOptions.Limit(0L, 3L)).build()).get();
+     * assertArrayEquals(new String[] {"a", "3", "2"}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sortReadOnly(String key, SortClusterOptions sortClusterOptions); + + /** + * 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, + * SortClusterOptions)} or {@link #sortReadOnly(String, SortClusterOptions)}. + * + * @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 sortClusterOptions The {@link SortClusterOptions}. + * @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",
+     *          SortClusterOptions.builder().alpha().orderBy(DESC)
+     *              .limit(new SortBaseOptions.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, SortClusterOptions sortClusterOptions); } 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 4823f08a09..44e53fb298 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.SortOptions; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** @@ -132,4 +134,74 @@ CompletableFuture copy( * } */ CompletableFuture randomKey(); + + /** + * 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, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return An 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", SortOptions.builder().byPattern("user:*->age")
+     *                  .getPattern("user:*->name").build()).get();
+     * assertArrayEquals(new String[] {"Bob", "Alice"}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sort(String key, SortOptions sortOptions); + + /** + * 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 sortOptions The {@link SortOptions}. + * @return An 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", SortOptions.builder().byPattern("user:*->age")
+     *                  .getPattern("user:*->name").build()).get();
+     * assertArrayEquals(new String[] {"Bob", "Alice"}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sortReadOnly(String key, SortOptions sortOptions); + + /** + * 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, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @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",
+     *          SortOptions.builder().byPattern("user:*->age").getPattern("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, SortOptions sortOptions); } 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 ddf145c8d6..d7ccb07bbf 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -12,6 +12,7 @@ import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; import static glide.api.commands.StringBaseCommands.LEN_REDIS_API; import static glide.api.models.commands.RangeOptions.createZRangeArgs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; @@ -143,6 +144,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; @@ -4752,6 +4755,57 @@ public T sunion(@NonNull String[] keys) { 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 - An 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 sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements. + * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @return Command Response - An 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 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)} or {@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(); + } + /** 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/ClusterTransaction.java b/java/client/src/main/java/glide/api/models/ClusterTransaction.java index e2c4820057..43f614e04e 100644 --- a/java/client/src/main/java/glide/api/models/ClusterTransaction.java +++ b/java/client/src/main/java/glide/api/models/ClusterTransaction.java @@ -1,7 +1,16 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.utils.ArrayTransformUtils.concatenateArrays; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; + +import glide.api.models.commands.SortClusterOptions; import lombok.AllArgsConstructor; +import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; +import redis_request.RedisRequestOuterClass; /** * Extends BaseTransaction class for cluster mode commands. Transactions allow the execution of a @@ -27,4 +36,68 @@ public class ClusterTransaction extends BaseTransaction { protected ClusterTransaction getThis() { 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, SortClusterOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - An Array of sorted elements. + */ + public ClusterTransaction sort( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + RedisRequestOuterClass.Command.ArgsArray commandArgs = + buildArgs(ArrayUtils.addFirst(sortClusterOptions.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.
+ * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - An Array of sorted elements. + */ + public ClusterTransaction sortReadOnly( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + RedisRequestOuterClass.Command.ArgsArray commandArgs = + buildArgs(ArrayUtils.addFirst(sortClusterOptions.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, + * SortClusterOptions)} or {@link #sortReadOnly(String, SortClusterOptions)}. + * + * @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 sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public ClusterTransaction sortStore( + @NonNull String key, + @NonNull String destination, + @NonNull SortClusterOptions sortClusterOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + RedisRequestOuterClass.Command.ArgsArray commandArgs = + buildArgs( + concatenateArrays(new String[] {key}, sortClusterOptions.toArgs(), storeArguments)); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return this; + } } 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 835fdc98e9..6ac58a33d7 100644 --- a/java/client/src/main/java/glide/api/models/Transaction.java +++ b/java/client/src/main/java/glide/api/models/Transaction.java @@ -3,10 +3,16 @@ import static glide.api.commands.GenericBaseCommands.REPLACE_REDIS_API; import static glide.api.commands.GenericCommands.DB_REDIS_API; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; +import static glide.utils.ArrayTransformUtils.concatenateArrays; import static redis_request.RedisRequestOuterClass.RequestType.Copy; 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.SortOptions; import lombok.AllArgsConstructor; import lombok.NonNull; import org.apache.commons.lang3.ArrayUtils; @@ -111,4 +117,58 @@ public Transaction copy( protobufTransaction.addCommands(buildCommand(Copy, 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, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return Command Response - An Array of sorted elements. + */ + public Transaction sort(@NonNull String key, @NonNull SortOptions sortOptions) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sortOptions.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.
+ * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return Command Response - An Array of sorted elements. + */ + public Transaction sortReadOnly(@NonNull String key, @NonNull SortOptions sortOptions) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sortOptions.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, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @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 SortOptions sortOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + ArgsArray arguments = + buildArgs(concatenateArrays(new String[] {key}, sortOptions.toArgs(), storeArguments)); + 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..3955acad71 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java @@ -0,0 +1,109 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to sort, sortReadOnly, and sortStore commands + * + * @see redis.io and redis.io + */ +@SuperBuilder +public abstract class SortBaseOptions { + /** + * 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 {@link 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 isAlpha; + + public abstract static class SortBaseOptionsBuilder< + C extends SortBaseOptions, B extends SortBaseOptionsBuilder> { + public B alpha() { + this.isAlpha = true; + return self(); + } + } + + /** + * 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 (isAlpha) { + optionArgs.add(ALPHA_COMMAND_STRING); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java b/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java new file mode 100644 index 0000000000..8d8a2a77e4 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.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, SortClusterOptions)}, {@link + * GenericBaseCommands#sortReadOnly(String, SortClusterOptions)}, and {@link + * GenericBaseCommands#sortStore(String, String, SortClusterOptions)} + */ +@SuperBuilder +public class SortClusterOptions extends SortBaseOptions {} 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..1044bc03ba --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortOptions.java @@ -0,0 +1,77 @@ +/** 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.Singular; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericCommands#sort(String, SortOptions)}, {@link + * GenericCommands#sortReadOnly(String, SortOptions)}, and {@link GenericCommands#sortStore(String, + * String, SortOptions)} + * + * @see redis.io and redis.io + */ +@SuperBuilder +public class SortOptions extends SortBaseOptions { + /** + * 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.
+ * + * @see valkey.io for more information. + */ + @Singular private final List 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) { + getPatterns.stream() + .forEach(getPattern -> optionArgs.addAll(List.of(GET_COMMAND_STRING, getPattern))); + } + + 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 6cecdd1da1..d8e4014504 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -20,6 +20,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.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.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; @@ -184,6 +189,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; @@ -255,6 +262,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.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; @@ -6801,4 +6810,131 @@ public void restore_with_restoreOptions_returns_success() { assertEquals(testResponse, response); assertEquals(OK, response.get()); } + + @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 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, + SortOptions.builder() + .alpha() + .limit(new SortBaseOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPattern(getPattern) + .byPattern(byPattern) + .build()); + 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"; + 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, SortOptions.builder().getPattern(getPattern).byPattern(byPattern).build()); + String[] 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 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, + STORE_COMMAND_STRING, + destKey + }; + 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, + SortOptions.builder() + .alpha() + .limit(new SortBaseOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPattern(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 be31009fae..26a3cef828 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -5,6 +5,10 @@ import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.models.commands.FlushMode.ASYNC; import static glide.api.models.commands.FlushMode.SYNC; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +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.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; @@ -41,6 +45,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RandomKey; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Time; import static redis_request.RedisRequestOuterClass.RequestType.UnWatch; @@ -48,6 +54,8 @@ import glide.api.models.ClusterValue; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortBaseOptions.Limit; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.commands.function.FunctionLoadOptions; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -1898,4 +1906,195 @@ public void randomKey() { // verify assertEquals(testResponse, response); } + + @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, + SortClusterOptions.builder() + .alpha() + .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, + SortClusterOptions.builder() + .alpha() + .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, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + STORE_COMMAND_STRING, + destKey + }; + 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, + SortClusterOptions.builder() + .alpha() + .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/ClusterTransactionTests.java b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java new file mode 100644 index 0000000000..389c66f654 --- /dev/null +++ b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java @@ -0,0 +1,94 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models; + +import static glide.api.models.TransactionTests.buildArgs; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.OrderBy.ASC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; + +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortClusterOptions; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import redis_request.RedisRequestOuterClass; + +public class ClusterTransactionTests { + private static Stream getTransactionBuilders() { + return Stream.of( + Arguments.of(new ClusterTransaction()), Arguments.of(new ClusterTransaction())); + } + + @ParameterizedTest + @MethodSource("getTransactionBuilders") + public void cluster_transaction_builds_protobuf_request(ClusterTransaction transaction) { + List> + results = new LinkedList<>(); + + transaction.sortReadOnly( + "key1", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", LIMIT_COMMAND_STRING, "0", "1", ASC.toString(), ALPHA_COMMAND_STRING))); + + transaction.sort( + "key1", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", LIMIT_COMMAND_STRING, "0", "1", ASC.toString(), ALPHA_COMMAND_STRING))); + + transaction.sortStore( + "key1", + "key2", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + ASC.toString(), + ALPHA_COMMAND_STRING, + STORE_COMMAND_STRING, + "key2"))); + + var protobufTransaction = transaction.getProtobufTransaction().build(); + + for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { + RedisRequestOuterClass.Command protobuf = protobufTransaction.getCommands(idx); + + assertEquals(results.get(idx).getLeft(), protobuf.getRequestType()); + assertEquals( + results.get(idx).getRight().getArgsCount(), protobuf.getArgsArray().getArgsCount()); + assertEquals(results.get(idx).getRight(), protobuf.getArgsArray()); + } + } +} 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 a3f47e2e61..15bf21a04d 100644 --- a/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java +++ b/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java @@ -4,11 +4,21 @@ import static glide.api.commands.GenericBaseCommands.REPLACE_REDIS_API; import static glide.api.commands.GenericCommands.DB_REDIS_API; import static glide.api.models.TransactionTests.buildArgs; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.Limit; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.BY_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.GET_COMMAND_STRING; import static org.junit.jupiter.api.Assertions.assertEquals; import static redis_request.RedisRequestOuterClass.RequestType.Copy; 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.SortOptions; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.tuple.Pair; @@ -29,6 +39,139 @@ public void standalone_transaction_commands() { transaction.copy("key1", "key2", 1, true); results.add(Pair.of(Copy, buildArgs("key1", "key2", DB_REDIS_API, "1", REPLACE_REDIS_API))); + transaction.sort( + "key1", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("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", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("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", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("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", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("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", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2", + STORE_COMMAND_STRING, + "key2"))); + transaction.sortStore( + "key1", + "key2", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("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", + STORE_COMMAND_STRING, + "key2"))); + 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 92ec6102a6..7b4218e47d 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -18,6 +18,7 @@ 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.SortBaseOptions.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; @@ -159,6 +160,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; @@ -1097,6 +1100,13 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.sunion(new String[] {"key1", "key2"}); results.add(Pair.of(SUnion, buildArgs("key1", "key2"))); + transaction.sort("key1"); + results.add(Pair.of(Sort, buildArgs("key1"))); + transaction.sortReadOnly("key1"); + results.add(Pair.of(SortReadOnly, buildArgs("key1"))); + transaction.sortStore("key1", "key2"); + results.add(Pair.of(Sort, buildArgs("key1", STORE_COMMAND_STRING, "key2"))); + 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 5842f63ddf..5f5d97ec73 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -5890,4 +5890,37 @@ public void test_dump_restore_withOptions(BaseClient client) { .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[] key2LpushArgs = {"2", "1", "a", "x", "c", "4", "3"}; + + assertArrayEquals(new String[0], client.sort(key3).get()); + assertEquals(4, client.lpush(key1, key1LpushArgs).get()); + assertArrayEquals(key1AscendingList, client.sort(key1).get()); + + // SORT_R0 + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals(new String[0], client.sortReadOnly(key3).get()); + assertArrayEquals(key1AscendingList, client.sortReadOnly(key1).get()); + } + + // SORT with STORE + assertEquals(4, client.sortStore(key1, key3).get()); + assertArrayEquals(key1AscendingList, client.lrange(key3, 0, -1).get()); + + // Exceptions + // SORT with strings require ALPHA + assertEquals(7, client.lpush(key2, key2LpushArgs).get()); + 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 6d44d664dd..37fe237d5f 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -106,6 +106,8 @@ private static Object[] genericCommands(BaseTransaction transaction) { 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) @@ -127,7 +129,11 @@ 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) + .sortStore(genericKey3, genericKey4) + .lrange(genericKey4, 0, -1); if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { transaction @@ -137,7 +143,8 @@ 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); } if (REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0")) { @@ -170,6 +177,10 @@ 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) + 3L, // sortStore(genericKey3, genericKey4) + ascendingList, // lrange(genericKey4, 0, -1) }; if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { @@ -184,6 +195,7 @@ private static Object[] genericCommands(BaseTransaction transaction) { false, // pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) -2L, // expiretime(genericKey1) -2L, // pexpiretime(genericKey1) + ascendingList, // sortReadOnly(genericKey3) }); } diff --git a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java index d1f1eb4452..2bd943a15a 100644 --- a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java +++ b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java @@ -4,8 +4,10 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestUtilities.assertDeepEquals; import static glide.api.BaseClient.OK; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; +import static glide.utils.ArrayTransformUtils.concatenateArrays; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -15,6 +17,7 @@ import glide.TransactionTestUtilities.TransactionBuilder; import glide.api.RedisClusterClient; import glide.api.models.ClusterTransaction; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -247,4 +250,40 @@ public void unwatch() { assertEquals(foobarString, clusterClient.get(key1).get()); assertEquals(foobarString, clusterClient.get(key2).get()); } + + @Test + @SneakyThrows + public void sort() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String[] descendingList = new String[] {"3", "2", "1"}; + ClusterTransaction transaction = new ClusterTransaction(); + transaction + .lpush(key1, new String[] {"3", "1", "2"}) + .sort(key1, SortClusterOptions.builder().orderBy(DESC).build()) + .sortStore(key1, key2, SortClusterOptions.builder().orderBy(DESC).build()) + .lrange(key2, 0, -1); + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction.sortReadOnly(key1, SortClusterOptions.builder().orderBy(DESC).build()); + } + + Object[] results = clusterClient.exec(transaction).get(); + Object[] expectedResult = + new Object[] { + 3L, // lpush(key1, new String[] {"3", "1", "2"}) + descendingList, // sort(key1, SortClusterOptions.builder().orderBy(DESC).build()) + 3L, // sortStore(key1, key2, DESC)) + descendingList, // lrange(key2, 0, -1) + }; + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResult = + concatenateArrays( + expectedResult, new Object[] {descendingList} // sortReadOnly(key1, DESC) + ); + } + + assertDeepEquals(expectedResult, results); + } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index dfab0682f7..8922482409 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -24,6 +24,7 @@ import static glide.api.models.commands.InfoOptions.Section.SERVER; import static glide.api.models.commands.InfoOptions.Section.STATS; import static glide.api.models.commands.ScoreFilter.MAX; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; import static glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; @@ -46,6 +47,8 @@ import glide.api.models.commands.InfoOptions; import glide.api.models.commands.ListDirection; import glide.api.models.commands.RangeOptions.RangeByIndex; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.configuration.RequestRoutingConfiguration.Route; @@ -792,7 +795,12 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { Arguments.of("msetnx", null, clusterClient.msetnx(Map.of("abc", "def", "ghi", "jkl"))), Arguments.of("lcs", "7.0.0", clusterClient.lcs("abc", "def")), Arguments.of("lcsLEN", "7.0.0", clusterClient.lcsLen("abc", "def")), - Arguments.of("sunion", "1.0.0", clusterClient.sunion(new String[] {"abc", "def", "ghi"}))); + Arguments.of("sunion", "1.0.0", clusterClient.sunion(new String[] {"abc", "def", "ghi"})), + Arguments.of("sortStore", "1.0.0", clusterClient.sortStore("abc", "def")), + Arguments.of( + "sortStore", + "1.0.0", + clusterClient.sortStore("abc", "def", SortClusterOptions.builder().alpha().build()))); } @SneakyThrows @@ -1607,4 +1615,90 @@ public void randomKey() { // uncomment when this is completed: https://github.com/amazon-contributing/redis-rs/pull/153 // assertNull(clusterClient.randomKey().get()); } + + @Test + @SneakyThrows + public void sort() { + 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], clusterClient.sort(key3).get()); + assertEquals(4, clusterClient.lpush(key1, key1LpushArgs).get()); + assertArrayEquals( + new String[0], + clusterClient + .sort( + key1, SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 0L)).build()) + .get()); + assertArrayEquals( + key1DescendingList, + clusterClient.sort(key1, SortClusterOptions.builder().orderBy(DESC).build()).get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + clusterClient + .sort( + key1, SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 2L)).build()) + .get()); + assertEquals(7, clusterClient.lpush(key2, key2LpushArgs).get()); + assertArrayEquals( + key2DescendingListSubset, + clusterClient + .sort( + key2, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + + // SORT_R0 + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals( + key1DescendingList, + clusterClient + .sortReadOnly(key1, SortClusterOptions.builder().orderBy(DESC).build()) + .get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + clusterClient + .sortReadOnly( + key1, + SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 2L)).build()) + .get()); + assertArrayEquals( + key2DescendingListSubset, + clusterClient + .sortReadOnly( + key2, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + } + + // SORT with STORE + assertEquals( + 4, + clusterClient + .sortStore( + key2, + key3, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + assertArrayEquals(key2DescendingListSubset, clusterClient.lrange(key3, 0, -1).get()); + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 60a9d00599..d18642e058 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -18,9 +18,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.SortBaseOptions.Limit; +import static glide.api.models.commands.SortBaseOptions.OrderBy.ASC; +import static glide.api.models.commands.SortBaseOptions.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.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -31,10 +35,12 @@ import glide.api.RedisClient; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -737,4 +743,178 @@ public void randomkey() { assertEquals(OK, regularClient.flushall().get()); assertNull(regularClient.randomKey().get()); } + + @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, + SortOptions.builder().limit(new Limit(0L, 2L)).getPattern(namePattern).build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "Dave"}, + regularClient + .sort( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "40", "Charlie", "35"}, + regularClient + .sort( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(List.of(namePattern, agePattern)) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient.sort(listKey, SortOptions.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, SortOptions.builder().alpha().getPattern("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, + SortOptions.builder().byPattern(agePattern).getPattern(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, + SortOptions.builder().limit(new Limit(0L, 2L)).getPattern(namePattern).build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "Dave"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "40", "Charlie", "35"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(List.of(namePattern, agePattern)) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient + .sortReadOnly(listKey, SortOptions.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, SortOptions.builder().alpha().getPattern("missing").build()) + .get()); + + assertArrayEquals( + namesSortedByAge, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder().byPattern(agePattern).getPattern(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, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertEquals(missingListKey, regularClient.lpop(listKey).get()); + } + + // SORT with STORE + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortOptions.builder() + .limit(new Limit(0L, -1L)) + .orderBy(ASC) + .byPattern(agePattern) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey, 0, -1).get()); + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortOptions.builder().byPattern(agePattern).getPattern(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 e3d532d325..0543410ba2 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -5,6 +5,7 @@ import static glide.TestUtilities.assertDeepEquals; import static glide.TestUtilities.commonClientConfig; import static glide.api.BaseClient.OK; +import static glide.api.models.commands.SortBaseOptions.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; @@ -17,6 +18,7 @@ import glide.api.RedisClient; import glide.api.models.Transaction; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -328,4 +330,81 @@ public void unwatch() { assertEquals(foobarString, client.get(key1).get()); assertEquals(foobarString, client.get(key2).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, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .sort( + genericKey1, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()) + .sortStore( + genericKey1, + genericKey2, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .lrange(genericKey2, 0, -1) + .sortStore( + genericKey1, + genericKey2, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("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, SortOptions) + descendingListByAge, // sort(genericKey1, SortOptions) + 2L, // sortStore(genericKey1, genericKey2, SortOptions) + ascendingListByAge, // lrange(genericKey4, 0, -1) + 2L, // sortStore(genericKey1, genericKey2, SortOptions) + descendingListByAge, // lrange(genericKey2, 0, -1) + }; + + assertArrayEquals(expectedResults, client.exec(transaction1).get()); + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction2 + .sortReadOnly( + genericKey1, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .sortReadOnly( + genericKey1, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()); + + expectedResults = + new Object[] { + ascendingListByAge, // sortReadOnly(genericKey1, SortOptions) + descendingListByAge, // sortReadOnly(genericKey1, SortOptions) + }; + + assertArrayEquals(expectedResults, client.exec(transaction2).get()); + } + } }