From 7299684e24780af06945ed4f8aa493fc2062c13f Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 7 Jun 2024 11:56:04 -0700 Subject: [PATCH 1/5] Java: Add `FUNCTION FLUSH` command. (#1533) * java: Add `FUNCTION FLUSH` command. (#288) Signed-off-by: Yury-Fridlyand Co-authored-by: Andrew Carbonetto --- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + .../src/main/java/glide/api/RedisClient.java | 13 +++ .../java/glide/api/RedisClusterClient.java | 25 +++++ .../ScriptingAndFunctionsClusterCommands.java | 69 ++++++++++++- .../ScriptingAndFunctionsCommands.java | 31 ++++++ .../glide/api/models/BaseTransaction.java | 27 +++++ .../glide/api/models/commands/FlushMode.java | 22 +++-- .../test/java/glide/api/RedisClientTest.java | 52 +++++++++- .../glide/api/RedisClusterClientTest.java | 99 +++++++++++++++++-- .../glide/api/models/TransactionTests.java | 5 + .../java/glide/TransactionTestUtilities.java | 11 ++- .../test/java/glide/cluster/CommandTests.java | 28 ++---- .../java/glide/standalone/CommandTests.java | 12 +-- 14 files changed, 350 insertions(+), 48 deletions(-) diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 33f73366e8..52cd18cbb7 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -192,6 +192,7 @@ enum RequestType { FunctionLoad = 150; FunctionList = 151; FunctionDelete = 152; + FunctionFlush = 153; LMPop = 155; ExpireTime = 156; PExpireTime = 157; diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index b916f9b7c7..c274c1289f 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -162,6 +162,7 @@ pub enum RequestType { FunctionLoad = 150, FunctionList = 151, FunctionDelete = 152, + FunctionFlush = 153, LMPop = 155, ExpireTime = 156, PExpireTime = 157, @@ -343,6 +344,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::FunctionLoad => RequestType::FunctionLoad, ProtobufRequestType::FunctionList => RequestType::FunctionList, ProtobufRequestType::FunctionDelete => RequestType::FunctionDelete, + ProtobufRequestType::FunctionFlush => RequestType::FunctionFlush, ProtobufRequestType::BitPos => RequestType::BitPos, ProtobufRequestType::BitOp => RequestType::BitOp, ProtobufRequestType::HStrlen => RequestType::HStrlen, @@ -521,6 +523,7 @@ impl RequestType { RequestType::FunctionLoad => Some(get_two_word_command("FUNCTION", "LOAD")), RequestType::FunctionList => Some(get_two_word_command("FUNCTION", "LIST")), RequestType::FunctionDelete => Some(get_two_word_command("FUNCTION", "DELETE")), + RequestType::FunctionFlush => Some(get_two_word_command("FUNCTION", "FLUSH")), RequestType::BitPos => Some(cmd("BITPOS")), RequestType::BitOp => Some(cmd("BITOP")), RequestType::HStrlen => Some(cmd("HSTRLEN")), diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index c53f48b902..a61280a879 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -18,6 +18,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; @@ -233,6 +234,18 @@ public CompletableFuture[]> functionList( response -> handleFunctionListResponse(handleArrayResponse(response))); } + @Override + public CompletableFuture functionFlush() { + return commandManager.submitNewCommand( + FunctionFlush, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture functionFlush(@NonNull FlushMode mode) { + return commandManager.submitNewCommand( + FunctionFlush, new String[] {mode.toString()}, this::handleStringResponse); + } + @Override public CompletableFuture functionDelete(@NonNull String libName) { return commandManager.submitNewCommand( diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index b02c66685c..0e88aea23e 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -20,6 +20,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; @@ -507,6 +508,30 @@ public CompletableFuture[]>> functionList( response -> handleFunctionListResponse(response, route)); } + @Override + public CompletableFuture functionFlush() { + return commandManager.submitNewCommand( + FunctionFlush, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture functionFlush(@NonNull FlushMode mode) { + return commandManager.submitNewCommand( + FunctionFlush, new String[] {mode.toString()}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionFlush(@NonNull Route route) { + return commandManager.submitNewCommand( + FunctionFlush, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture functionFlush(@NonNull FlushMode mode, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionFlush, new String[] {mode.toString()}, route, this::handleStringResponse); + } + @Override public CompletableFuture functionDelete(@NonNull String libName) { return commandManager.submitNewCommand( diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java index a868a8d82e..f9d6f5b754 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java @@ -2,6 +2,7 @@ package glide.api.commands; import glide.api.models.ClusterValue; +import glide.api.models.commands.FlushMode; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -168,6 +169,72 @@ CompletableFuture[]>> functionList( CompletableFuture[]>> functionList( String libNamePattern, boolean withCode, Route route); + /** + * Deletes all function libraries.
+ * The command will be routed to all primary nodes. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(); + + /** + * Deletes all function libraries.
+ * The command will be routed to all primary nodes. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush(SYNC).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(FlushMode mode); + + /** + * Deletes all function libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush(RANDOM).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(Route route); + + /** + * Deletes all function libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush(SYNC, RANDOM).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(FlushMode mode, Route route); + /** * Deletes a library and all its functions.
* The command will be routed to all primary nodes. @@ -190,8 +257,6 @@ CompletableFuture[]>> functionList( * @since Redis 7.0 and above. * @see redis.io for details. * @param libName The library name to delete. - * @param route Specifies the routing configuration for the command. The client will route the - * command to the nodes defined by route. * @return OK. * @example *
{@code
diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java
index 1097fdadd5..35b196c7ac 100644
--- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java
+++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java
@@ -1,6 +1,7 @@
 /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
 package glide.api.commands;
 
+import glide.api.models.commands.FlushMode;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 
@@ -82,6 +83,36 @@ public interface ScriptingAndFunctionsCommands {
      */
     CompletableFuture[]> functionList(String libNamePattern, boolean withCode);
 
+    /**
+     * Deletes all function libraries.
+     *
+     * @since Redis 7.0 and above.
+     * @see redis.io for details.
+     * @return OK.
+     * @example
+     *     
{@code
+     * String response = client.functionFlush().get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(); + + /** + * Deletes all function libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return OK. + * @example + *
{@code
+     * String response = client.functionFlush(SYNC).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionFlush(FlushMode mode); + /** * Deletes a library and all its functions. * 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 9d4fa2f35b..eadf0f4e50 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -49,6 +49,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; @@ -4152,6 +4153,32 @@ public T bitfieldReadOnly( return getThis(); } + /** + * Deletes all function libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @return Command Response - OK. + */ + public T functionFlush() { + protobufTransaction.addCommands(buildCommand(FunctionFlush)); + return getThis(); + } + + /** + * Deletes all function libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param mode The flushing mode, could be either {@link FlushMode#SYNC} or {@link + * FlushMode#ASYNC}. + * @return Command Response - OK. + */ + public T functionFlush(@NonNull FlushMode mode) { + protobufTransaction.addCommands(buildCommand(FunctionFlush, buildArgs(mode.toString()))); + return getThis(); + } + /** * Deletes a library and all its functions. * diff --git a/java/client/src/main/java/glide/api/models/commands/FlushMode.java b/java/client/src/main/java/glide/api/models/commands/FlushMode.java index 9de501f6af..69fc260ecd 100644 --- a/java/client/src/main/java/glide/api/models/commands/FlushMode.java +++ b/java/client/src/main/java/glide/api/models/commands/FlushMode.java @@ -4,17 +4,27 @@ import glide.api.RedisClient; import glide.api.RedisClusterClient; import glide.api.models.configuration.RequestRoutingConfiguration.Route; +import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; +// TODO add links to script flush /** - * Defines flushing mode for FLUSHALL command implemented by {@link - * RedisClient#flushall(FlushMode)}, {@link RedisClusterClient#flushall(FlushMode)}, and {@link - * RedisClusterClient#flushall(FlushMode, Route)}. + * Defines flushing mode for: * - * @see valkey.io + *
    + *
  • FLUSHALL command implemented by {@link RedisClient#flushall(FlushMode)}, + * {@link RedisClusterClient#flushall(FlushMode)}, and {@link + * RedisClusterClient#flushall(FlushMode, SingleNodeRoute)}. + *
  • FUNCTION FLUSH command implemented by {@link + * RedisClient#functionFlush(FlushMode)}, {@link RedisClusterClient#functionFlush(FlushMode)}, + * and {@link RedisClusterClient#functionFlush(FlushMode, Route)}. + *
+ * + * @see valkey.io and valkey.io */ public enum FlushMode { - /** Flushes the databases synchronously. */ + /** Flushes synchronously. */ SYNC, - /** Flushes the databases asynchronously. */ + /** Flushes asynchronously. */ ASYNC } diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index cbb6a90fe7..28078f64fe 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -9,6 +9,7 @@ import static glide.api.commands.SortedSetBaseCommands.LIMIT_REDIS_API; 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.FlushMode.ASYNC; import static glide.api.models.commands.FlushMode.SYNC; import static glide.api.models.commands.LInsertOptions.InsertPosition.BEFORE; import static glide.api.models.commands.ScoreFilter.MAX; @@ -76,6 +77,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; @@ -199,6 +201,7 @@ import glide.api.models.Transaction; import glide.api.models.commands.ConditionalChange; import glide.api.models.commands.ExpireOptions; +import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.ListDirection; import glide.api.models.commands.RangeOptions; @@ -4990,15 +4993,57 @@ public void functionList_with_pattern_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void functionFlush_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionFlush_with_mode_returns_success() { + // setup + FlushMode mode = ASYNC; + String[] args = new String[] {mode.toString()}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(mode); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void functionDelete_returns_success() { // setup String libName = "GLIDE"; String[] args = new String[] {libName}; - String value = OK; CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); + testResponse.complete(OK); // match on protobuf request when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), any())) @@ -5010,7 +5055,7 @@ public void functionDelete_returns_success() { // verify assertEquals(testResponse, response); - assertEquals(value, payload); + assertEquals(OK, payload); } @SneakyThrows @@ -5421,7 +5466,6 @@ public void lset_returns_success() { long index = 0; String element = "two"; String[] arguments = new String[] {key, "0", element}; - CompletableFuture testResponse = new CompletableFuture<>(); testResponse.complete(OK); diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index c20aa9d6be..7bf0fbffb9 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -3,6 +3,7 @@ import static glide.api.BaseClient.OK; 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.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; @@ -27,6 +28,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; @@ -37,6 +39,7 @@ import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; +import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.function.FunctionLoadOptions; import glide.api.models.configuration.RequestRoutingConfiguration.Route; @@ -1316,15 +1319,100 @@ public void functionList_with_pattern_and_route_returns_success() { assertEquals(value, payload.getSingleValue()); } + @SneakyThrows + @Test + public void functionFlush_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionFlush_with_mode_returns_success() { + // setup + FlushMode mode = ASYNC; + String[] args = new String[] {mode.toString()}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(mode); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionFlush_with_route_returns_success() { + // setup + String[] args = new String[0]; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionFlush_with_mode_and_route_returns_success() { + // setup + FlushMode mode = ASYNC; + String[] args = new String[] {mode.toString()}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionFlush), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionFlush(mode, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void functionDelete_returns_success() { // setup String libName = "GLIDE"; String[] args = new String[] {libName}; - String value = OK; CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); + testResponse.complete(OK); // match on protobuf request when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), any())) @@ -1336,7 +1424,7 @@ public void functionDelete_returns_success() { // verify assertEquals(testResponse, response); - assertEquals(value, payload); + assertEquals(OK, payload); } @SneakyThrows @@ -1345,9 +1433,8 @@ public void functionDelete_with_route_returns_success() { // setup String libName = "GLIDE"; String[] args = new String[] {libName}; - String value = OK; CompletableFuture testResponse = new CompletableFuture<>(); - testResponse.complete(value); + testResponse.complete(OK); // match on protobuf request when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), eq(RANDOM), any())) @@ -1359,6 +1446,6 @@ public void functionDelete_with_route_returns_success() { // verify assertEquals(testResponse, response); - assertEquals(value, payload); + assertEquals(OK, payload); } } 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 bf9169f2f5..7a52c458f6 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -59,6 +59,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; @@ -947,6 +948,10 @@ 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.functionFlush().functionFlush(ASYNC); + results.add(Pair.of(FunctionFlush, buildArgs())); + results.add(Pair.of(FunctionFlush, buildArgs("ASYNC"))); + transaction.functionDelete("LIB"); results.add(Pair.of(FunctionDelete, buildArgs("LIB"))); diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 482416c010..5e532ec977 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -4,6 +4,7 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.api.BaseClient.OK; import static glide.api.models.commands.FlushMode.ASYNC; +import static glide.api.models.commands.FlushMode.SYNC; 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; @@ -704,24 +705,24 @@ private static Object[] scriptingAndFunctionsCommands(BaseTransaction transac }; transaction - .customCommand(new String[] {"function", "flush", "sync"}) + .functionFlush(SYNC) .functionList(false) - .functionList(true) .functionLoad(code, false) .functionLoad(code, true) .functionList("otherLib", false) .functionList("mylib1T", true) - .functionDelete("mylib1T"); + .functionDelete("mylib1T") + .functionList(true); return new Object[] { - OK, // customCommand("function", "flush", "sync") + OK, // functionFlush(SYNC) new Map[0], // functionList(false) - new Map[0], // functionList(true) "mylib1T", // functionLoad(code, false) "mylib1T", // functionLoad(code, true) new Map[0], // functionList("otherLib", false) expectedLibData, // functionList("mylib1T", true) OK, // functionDelete("mylib1T") + new Map[0], // functionList(true) }; } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 16135e3dea..20a87596cc 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -9,6 +9,8 @@ import static glide.TestUtilities.getValueFromInfo; import static glide.TestUtilities.parseInfoResponseToMap; import static glide.api.BaseClient.OK; +import static glide.api.models.commands.FlushMode.ASYNC; +import static glide.api.models.commands.FlushMode.SYNC; import static glide.api.models.commands.InfoOptions.Section.CLIENTS; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; import static glide.api.models.commands.InfoOptions.Section.COMMANDSTATS; @@ -790,8 +792,8 @@ public void flushall() { var route = new SlotKeyRoute("key", PRIMARY); assertEquals(OK, clusterClient.flushall().get()); assertEquals(OK, clusterClient.flushall(route).get()); - assertEquals(OK, clusterClient.flushall(FlushMode.ASYNC).get()); - assertEquals(OK, clusterClient.flushall(FlushMode.ASYNC, route).get()); + assertEquals(OK, clusterClient.flushall(ASYNC).get()); + assertEquals(OK, clusterClient.flushall(ASYNC, route).get()); var replicaRoute = new SlotKeyRoute("key", REPLICA); // command should fail on a replica, because it is read-only @@ -811,19 +813,13 @@ public void flushall() { public void function_commands(boolean singleNodeRoute) { assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); - // TODO use FUNCTION FLUSH - assertEquals( - OK, - clusterClient - .customCommand(new String[] {"FUNCTION", "FLUSH", "SYNC"}) - .get() - .getSingleValue()); - String libName = "mylib1c_" + singleNodeRoute; String funcName = "myfunc1c_" + singleNodeRoute; + String code = generateLuaLibCode(libName, List.of(funcName)); Route route = singleNodeRoute ? new SlotKeyRoute("1", PRIMARY) : ALL_PRIMARIES; + assertEquals(OK, clusterClient.functionFlush(SYNC, route).get()); assertEquals(libName, clusterClient.functionLoad(code, false, route).get()); // TODO test function with FCALL when fixed in redis-rs and implemented @@ -921,7 +917,7 @@ public void function_commands(boolean singleNodeRoute) { // TODO test with FCALL - // TODO FUNCTION FLUSH at the end + assertEquals(OK, clusterClient.functionFlush(route).get()); } @SneakyThrows @@ -929,13 +925,7 @@ public void function_commands(boolean singleNodeRoute) { public void function_commands() { assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); - // TODO use FUNCTION FLUSH - assertEquals( - OK, - clusterClient - .customCommand(new String[] {"FUNCTION", "FLUSH", "SYNC"}) - .get() - .getSingleValue()); + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); String libName = "mylib1c"; String funcName = "myfunc1c"; @@ -1000,6 +990,6 @@ public void function_commands() { // TODO test with FCALL - // TODO FUNCTION FLUSH at the end + assertEquals(OK, clusterClient.functionFlush(ASYNC).get()); } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index f1b1c63d28..9ee684cacf 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -8,6 +8,8 @@ import static glide.TestUtilities.getValueFromInfo; import static glide.TestUtilities.parseInfoResponseToMap; import static glide.api.BaseClient.OK; +import static glide.api.models.commands.FlushMode.ASYNC; +import static glide.api.models.commands.FlushMode.SYNC; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; import static glide.api.models.commands.InfoOptions.Section.CPU; import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; @@ -25,7 +27,6 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; import glide.api.RedisClient; -import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClientConfiguration; @@ -379,14 +380,14 @@ public void objectFreq() { @Test @SneakyThrows public void flushall() { - assertEquals(OK, regularClient.flushall(FlushMode.SYNC).get()); + assertEquals(OK, regularClient.flushall(SYNC).get()); // TODO replace with KEYS command when implemented Object[] keysAfter = (Object[]) regularClient.customCommand(new String[] {"keys", "*"}).get(); assertEquals(0, keysAfter.length); assertEquals(OK, regularClient.flushall().get()); - assertEquals(OK, regularClient.flushall(FlushMode.ASYNC).get()); + assertEquals(OK, regularClient.flushall(ASYNC).get()); } @SneakyThrows @@ -394,8 +395,7 @@ public void flushall() { public void function_commands() { assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); - // TODO use FUNCTION FLUSH - assertEquals(OK, regularClient.customCommand(new String[] {"FUNCTION", "FLUSH", "SYNC"}).get()); + assertEquals(OK, regularClient.functionFlush(SYNC).get()); String libName = "mylib1c"; String funcName = "myfunc1c"; @@ -457,6 +457,6 @@ public void function_commands() { flist, libName, expectedDescription, expectedFlags, Optional.of(newCode)); // TODO test with FCALL - // TODO FUNCTION FLUSH at the end + assertEquals(OK, regularClient.functionFlush(ASYNC).get()); } } From a72a75bebd4f57818c031daf4f7927eb3bb7c89c Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:58:09 -0700 Subject: [PATCH 2/5] Java: Adding command `COPY` (#1532) Java: Adding command COPY (#340) * Java: Adding command COPY --- glide-core/src/client/value_conversion.rs | 2 +- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + .../src/main/java/glide/api/BaseClient.java | 17 ++++ .../src/main/java/glide/api/RedisClient.java | 21 +++++ .../api/commands/GenericBaseCommands.java | 48 +++++++++++ .../glide/api/commands/GenericCommands.java | 44 +++++++++++ .../glide/api/models/BaseTransaction.java | 40 ++++++++++ .../java/glide/api/models/Transaction.java | 47 +++++++++++ .../test/java/glide/api/RedisClientTest.java | 79 +++++++++++++++++++ .../models/StandaloneTransactionTests.java | 5 ++ .../glide/api/models/TransactionTests.java | 5 ++ .../test/java/glide/SharedCommandTests.java | 31 ++++++++ .../java/glide/TransactionTestUtilities.java | 45 ++++++++--- .../test/java/glide/cluster/CommandTests.java | 3 +- .../java/glide/standalone/CommandTests.java | 49 ++++++++++++ .../glide/standalone/TransactionTests.java | 37 +++++++++ 17 files changed, 464 insertions(+), 13 deletions(-) diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 0d74d7cf7f..347fa11a54 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -698,7 +698,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { }), b"INCRBYFLOAT" | b"HINCRBYFLOAT" | b"ZINCRBY" => Some(ExpectedReturnType::Double), b"HEXISTS" | b"HSETNX" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT" - | b"SISMEMBER" | b"PERSIST" | b"SMOVE" | b"RENAMENX" | b"MOVE" => { + | b"SISMEMBER" | b"PERSIST" | b"SMOVE" | b"RENAMENX" | b"MOVE" | b"COPY" => { Some(ExpectedReturnType::Boolean) } b"SMISMEMBER" => Some(ExpectedReturnType::ArrayOfBools), diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 52cd18cbb7..7a8f3a2746 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -210,6 +210,7 @@ enum RequestType { BitFieldReadOnly = 173; Move = 174; SInterCard = 175; + Copy = 178; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index c274c1289f..8e20449c1d 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -180,6 +180,7 @@ pub enum RequestType { BitFieldReadOnly = 173, Move = 174, SInterCard = 175, + Copy = 178, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -362,6 +363,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::BitFieldReadOnly => RequestType::BitFieldReadOnly, ProtobufRequestType::Move => RequestType::Move, ProtobufRequestType::SInterCard => RequestType::SInterCard, + ProtobufRequestType::Copy => RequestType::Copy, ProtobufRequestType::Sort => RequestType::Sort, } } @@ -541,6 +543,7 @@ impl RequestType { RequestType::BitFieldReadOnly => Some(cmd("BITFIELD_RO")), RequestType::Move => Some(cmd("MOVE")), RequestType::SInterCard => Some(cmd("SINTERCARD")), + RequestType::Copy => Some(cmd("COPY")), RequestType::Sort => Some(cmd("SORT")), } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index ba6b594dcb..239b7db1f4 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -25,6 +25,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.BitFieldReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.BitOp; import static redis_request.RedisRequestOuterClass.RequestType.BitPos; +import static redis_request.RedisRequestOuterClass.RequestType.Copy; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; @@ -1733,4 +1734,20 @@ 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 copy( + @NonNull String source, @NonNull String destination, boolean replace) { + String[] arguments = new String[] {source, destination}; + if (replace) { + arguments = ArrayUtils.add(arguments, REPLACE_REDIS_API); + } + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture copy(@NonNull String source, @NonNull String destination) { + String[] arguments = new String[] {source, destination}; + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index a61280a879..9855e424e8 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -13,6 +13,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; +import static redis_request.RedisRequestOuterClass.RequestType.Copy; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.DBSize; import static redis_request.RedisRequestOuterClass.RequestType.Echo; @@ -43,6 +44,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 @@ -251,4 +253,23 @@ public CompletableFuture functionDelete(@NonNull String libName) { return commandManager.submitNewCommand( FunctionDelete, new String[] {libName}, this::handleStringResponse); } + + @Override + public CompletableFuture copy( + @NonNull String source, @NonNull String destination, long destinationDB) { + String[] arguments = + new String[] {source, destination, DB_REDIS_API, Long.toString(destinationDB)}; + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture copy( + @NonNull String source, @NonNull String destination, long destinationDB, boolean replace) { + String[] arguments = + new String[] {source, destination, DB_REDIS_API, Long.toString(destinationDB)}; + if (replace) { + arguments = ArrayUtils.add(arguments, REPLACE_REDIS_API); + } + return commandManager.submitNewCommand(Copy, arguments, this::handleBooleanResponse); + } } 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..18d07d8f0f 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -13,6 +13,8 @@ * @see Generic Commands */ public interface GenericBaseCommands { + /** Redis API keyword used to replace the destination key. */ + String REPLACE_REDIS_API = "REPLACE"; /** * Removes the specified keys from the database. A key is ignored if it does not @@ -542,4 +544,50 @@ CompletableFuture pexpireAt( * }
*/ CompletableFuture touch(String[] keys); + + /** + * Copies the value stored at the source to the destination key if the + * destination key does not yet exist. + * + * @apiNote When in cluster mode, both source and destination must map + * to the same hash slot. + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set("test1", "one").get();
+     * client.set("test2", "two").get();
+     * assert !client.copy("test1", "test2").get();
+     * assert client.copy("test1", "test2").get();
+     * }
+ */ + CompletableFuture copy(String source, String destination); + + /** + * Copies the value stored at the source to the destination key. When + * replace is true, removes the destination key first if it already + * exists, otherwise performs no action. + * + * @apiNote When in cluster mode, both source and destination must map + * to the same hash slot. + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param replace If the destination key should be removed before copying the value to it. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set("test1", "one").get();
+     * client.set("test2", "two").get();
+     * assert !client.copy("test1", "test2", false).get();
+     * assert client.copy("test1", "test2", true).get();
+     * }
+ */ + CompletableFuture copy(String source, String destination, boolean replace); } 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..27447f0c6c 100644 --- a/java/client/src/main/java/glide/api/commands/GenericCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericCommands.java @@ -10,6 +10,8 @@ * @see Generic Commands */ public interface GenericCommands { + /** Redis API keyword used to denote the destination db index. */ + String DB_REDIS_API = "DB"; /** * Executes a single command, without checking inputs. Every part of the command, including @@ -72,4 +74,46 @@ public interface GenericCommands { * } */ CompletableFuture move(String key, long dbIndex); + + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @param replace If the destination key should be removed before copying the value to it. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set("test1", "one").get();
+     * assert client.copy("test1", "test2", 1, false).get();
+     * }
+ */ + CompletableFuture copy( + String source, String destination, long destinationDB, boolean replace); + + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @return true if source was copied, false if source + * was not copied. + * @example + *
{@code
+     * client.set("test1", "one").get();
+     * assert client.copy("test1", "test2", 1).get();
+     * }
+ */ + CompletableFuture copy(String source, String destination, long destinationDB); } 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 eadf0f4e50..1dfe7fadc1 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.commands.GenericBaseCommands.REPLACE_REDIS_API; import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API; import static glide.api.commands.ListBaseCommands.COUNT_FOR_LIST_REDIS_API; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; @@ -37,6 +38,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; +import static redis_request.RedisRequestOuterClass.RequestType.Copy; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.DBSize; import static redis_request.RedisRequestOuterClass.RequestType.Decr; @@ -3457,6 +3459,44 @@ public T touch(@NonNull String[] keys) { return getThis(); } + /** + * Copies the value stored at the source to the destination key. When + * replace is true, removes the destination key first if it already + * exists, otherwise performs no action. + * + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param replace If the destination key should be removed before copying the value to it. + * @return Command Response - 1L if source was copied, 0L + * if source was not copied. + */ + public T copy(@NonNull String source, @NonNull String destination, boolean replace) { + String[] args = new String[] {source, destination}; + if (replace) { + args = ArrayUtils.add(args, REPLACE_REDIS_API); + } + ArgsArray commandArgs = buildArgs(args); + protobufTransaction.addCommands(buildCommand(Copy, commandArgs)); + return getThis(); + } + + /** + * Copies the value stored at the source to the destination key if the + * destination key does not yet exist. + * + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @return Command Response - true if source was copied, false + * if source was not copied. + */ + public T copy(@NonNull String source, @NonNull String destination) { + return copy(source, destination, false); + } + /** * Counts the number of set bits (population counting) in a string stored at key. * 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..835fdc98e9 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,15 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.commands.GenericBaseCommands.REPLACE_REDIS_API; +import static glide.api.commands.GenericCommands.DB_REDIS_API; +import static redis_request.RedisRequestOuterClass.RequestType.Copy; import static redis_request.RedisRequestOuterClass.RequestType.Move; import static redis_request.RedisRequestOuterClass.RequestType.Select; import lombok.AllArgsConstructor; +import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; import redis_request.RedisRequestOuterClass.Command.ArgsArray; /** @@ -64,4 +69,46 @@ public Transaction move(String key, long dbIndex) { protobufTransaction.addCommands(buildCommand(Move, commandArgs)); return this; } + + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @return Command Response - true if source was copied, false + * if source was not copied. + */ + public Transaction copy(@NonNull String source, @NonNull String destination, long destinationDB) { + return copy(source, destination, destinationDB, false); + } + + /** + * Copies the value stored at the source to the destination key on + * destinationDB. When replace is true, removes the destination + * key first if it already exists, otherwise performs no action. + * + * @since Redis 6.2.0 and above. + * @see redis.io for details. + * @param source The key to the source value. + * @param destination The key where the value should be copied to. + * @param destinationDB The alternative logical database index for the destination key. + * @param replace If the destination key should be removed before copying the value to it. + * @return Command Response - true if source was copied, false + * if source was not copied. + */ + public Transaction copy( + @NonNull String source, @NonNull String destination, long destinationDB, boolean replace) { + String[] args = new String[] {source, destination, DB_REDIS_API, Long.toString(destinationDB)}; + if (replace) { + args = ArrayUtils.add(args, REPLACE_REDIS_API); + } + ArgsArray commandArgs = buildArgs(args); + protobufTransaction.addCommands(buildCommand(Copy, commandArgs)); + return this; + } } diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 28078f64fe..e2effea9b2 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -2,6 +2,8 @@ package glide.api; import static glide.api.BaseClient.OK; +import static glide.api.commands.GenericBaseCommands.REPLACE_REDIS_API; +import static glide.api.commands.GenericCommands.DB_REDIS_API; import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API; import static glide.api.commands.ListBaseCommands.COUNT_FOR_LIST_REDIS_API; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; @@ -65,6 +67,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; +import static redis_request.RedisRequestOuterClass.RequestType.Copy; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.DBSize; import static redis_request.RedisRequestOuterClass.RequestType.Decr; @@ -5771,4 +5774,80 @@ public void move_returns_success() { assertEquals(testResponse, response); assertEquals(value, payload); } + + @SneakyThrows + @Test + public void copy_returns_success() { + // setup + String source = "testKey1"; + String destination = "testKey2"; + String[] arguments = new String[] {source, destination}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Copy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.copy(source, destination); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void copy_with_replace_returns_success() { + // setup + String source = "testKey1"; + String destination = "testKey2"; + String[] arguments = new String[] {source, destination, REPLACE_REDIS_API}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Copy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.copy(source, destination, true); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void copy_with_destinationDB_returns_success() { + // setup + String source = "testKey1"; + String destination = "testKey2"; + long destinationDB = 1; + String[] arguments = new String[] {source, destination, DB_REDIS_API, "1", REPLACE_REDIS_API}; + Boolean value = true; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Copy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.copy(source, destination, destinationDB, true); + Boolean payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, 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..a3f47e2e61 100644 --- a/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java +++ b/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java @@ -1,8 +1,11 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +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 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; @@ -23,6 +26,8 @@ public void standalone_transaction_commands() { results.add(Pair.of(Select, buildArgs("5"))); transaction.move("testKey", 2L); results.add(Pair.of(Move, buildArgs("testKey", "2"))); + transaction.copy("key1", "key2", 1, true); + results.add(Pair.of(Copy, buildArgs("key1", "key2", DB_REDIS_API, "1", REPLACE_REDIS_API))); var protobufTransaction = transaction.getProtobufTransaction().build(); 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 7a52c458f6..1807b9f12c 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.commands.GenericBaseCommands.REPLACE_REDIS_API; import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.LIMIT_REDIS_API; @@ -48,6 +49,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; +import static redis_request.RedisRequestOuterClass.RequestType.Copy; import static redis_request.RedisRequestOuterClass.RequestType.DBSize; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; @@ -955,6 +957,9 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.functionDelete("LIB"); results.add(Pair.of(FunctionDelete, buildArgs("LIB"))); + transaction.copy("key1", "key2", true); + results.add(Pair.of(Copy, buildArgs("key1", "key2", REPLACE_REDIS_API))); + 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 ecf75ff76e..0679f665c5 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -4950,4 +4950,35 @@ 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 copy(BaseClient client) { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in redis 6.2.0"); + // setup + String source = "{key}-1" + UUID.randomUUID(); + String destination = "{key}-2" + UUID.randomUUID(); + + // neither key exists, returns false + assertFalse(client.copy(source, destination, false).get()); + assertFalse(client.copy(source, destination).get()); + + // source exists, destination does not + client.set(source, "one"); + assertTrue(client.copy(source, destination, false).get()); + assertEquals("one", client.get(destination).get()); + + // setting new value for source + client.set(source, "two"); + + // both exists, no REPLACE + assertFalse(client.copy(source, destination).get()); + assertFalse(client.copy(source, destination, false).get()); + assertEquals("one", client.get(destination).get()); + + // both exists, with REPLACE + assertTrue(client.copy(source, destination, true).get()); + assertEquals("two", client.get(destination).get()); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 5e532ec977..05d0dd3bd7 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -98,6 +98,8 @@ 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(); transaction .set(genericKey1, value1) @@ -132,6 +134,14 @@ private static Object[] genericCommands(BaseTransaction transaction) { .pexpiretime(genericKey1); } + if (REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + transaction + .set(genericKey3, "value") + .set(genericKey4, "value2") + .copy(genericKey3, genericKey4, false) + .copy(genericKey3, genericKey4, true); + } + var expectedResults = new Object[] { OK, // set(genericKey1, value1) @@ -157,17 +167,30 @@ private static Object[] genericCommands(BaseTransaction transaction) { }; if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { - return concatenateArrays( - expectedResults, - new Object[] { - OK, // set(genericKey1, value1) - true, // expire(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) - true, // expireAt(genericKey1, 500, ExpireOptions.HAS_EXISTING_EXPIRY) - false, // pexpire(genericKey1, 42, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT) - false, // pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) - -2L, // expiretime(genericKey1) - -2L, // pexpiretime(genericKey1) - }); + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + OK, // set(genericKey1, value1) + true, // expire(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) + true, // expireAt(genericKey1, 500, ExpireOptions.HAS_EXISTING_EXPIRY) + false, // pexpire(genericKey1, 42, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT) + false, // pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) + -2L, // expiretime(genericKey1) + -2L, // pexpiretime(genericKey1) + }); + } + + if (REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + OK, // set(genericKey3, "value1") + OK, // set(genericKey4, "value2") + false, // copy(genericKey3, genericKey4, false) + true, // copy(genericKey3, genericKey4, true) + }); } return expectedResults; } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 20a87596cc..afc8c46f04 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -746,7 +746,8 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { clusterClient.blmove("abc", "def", ListDirection.LEFT, ListDirection.LEFT, 1)), Arguments.of("sintercard", "7.0.0", clusterClient.sintercard(new String[] {"abc", "def"})), Arguments.of( - "sintercard", "7.0.0", clusterClient.sintercard(new String[] {"abc", "def"}, 1))); + "sintercard", "7.0.0", clusterClient.sintercard(new String[] {"abc", "def"}, 1)), + Arguments.of("copy", "6.2.0", clusterClient.copy("abc", "def", true))); } @SneakyThrows diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 9ee684cacf..ed4743bb72 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -20,6 +20,7 @@ import static glide.cluster.CommandTests.EVERYTHING_INFO_SECTIONS; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -459,4 +460,52 @@ public void function_commands() { // TODO test with FCALL assertEquals(OK, regularClient.functionFlush(ASYNC).get()); } + + @Test + @SneakyThrows + public void copy() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0"), "This feature added in redis 6.2.0"); + // setup + String source = "{key}-1" + UUID.randomUUID(); + String destination = "{key}-2" + UUID.randomUUID(); + long index1 = 1; + long index2 = 2; + + try { + // neither key exists, returns false + assertFalse(regularClient.copy(source, destination, index1, false).get()); + + // source exists, destination does not + regularClient.set(source, "one").get(); + assertTrue(regularClient.copy(source, destination, index1, false).get()); + regularClient.select(1).get(); + assertEquals("one", regularClient.get(destination).get()); + + // new value for source key + regularClient.select(0).get(); + regularClient.set(source, "two").get(); + + // no REPLACE, copying to existing key on DB 0&1, non-existing key on DB 2 + assertFalse(regularClient.copy(source, destination, index1, false).get()); + assertTrue(regularClient.copy(source, destination, index2, false).get()); + + // new value only gets copied to DB 2 + regularClient.select(1).get(); + assertEquals("one", regularClient.get(destination).get()); + regularClient.select(2).get(); + assertEquals("two", regularClient.get(destination).get()); + + // both exists, with REPLACE, when value isn't the same, source always get copied to + // destination + regularClient.select(0).get(); + assertTrue(regularClient.copy(source, destination, index1, true).get()); + regularClient.select(1).get(); + assertEquals("two", regularClient.get(destination).get()); + } + + // switching back to db 0 + finally { + regularClient.select(0).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..a0adf6c8e0 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -227,4 +227,41 @@ public void WATCH_transaction_failure_returns_null() { assertEquals(OK, client.set("key", "foo").get()); assertNull(client.exec(transaction).get()); } + + @Test + @SneakyThrows + public void copy() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0")); + // setup + String copyKey1 = "{CopyKey}-1-" + UUID.randomUUID(); + String copyKey2 = "{CopyKey}-2-" + UUID.randomUUID(); + Transaction transaction = + new Transaction() + .copy(copyKey1, copyKey2, 1, false) + .set(copyKey1, "one") + .set(copyKey2, "two") + .copy(copyKey1, copyKey2, 1, false) + .copy(copyKey1, copyKey2, 1, true) + .copy(copyKey1, copyKey2, 2, true) + .select(1) + .get(copyKey2) + .select(2) + .get(copyKey2); + Object[] expectedResult = + new Object[] { + false, // copy(copyKey1, copyKey2, 1, false) + OK, // set(copyKey1, "one") + OK, // set(copyKey2, "two") + true, // copy(copyKey1, copyKey2, 1, false) + true, // copy(copyKey1, copyKey2, 1, true) + true, // copy(copyKey1, copyKey2, 2, true) + OK, // select(1) + "one", // get(copyKey2) + OK, // select(2) + "one", // get(copyKey2) + }; + + Object[] result = client.exec(transaction).get(); + assertArrayEquals(expectedResult, result); + } } From 2af6a18f6fbb372b31166237f1a0bf9615a73a6d Mon Sep 17 00:00:00 2001 From: adarovadya Date: Sat, 8 Jun 2024 18:00:41 +0300 Subject: [PATCH 3/5] Node: Add ZINTERSTORE command (#1513) * add zinterstore command node * Node: added zinterstore command * Node: added zinterstore command * Node: add ZINTERSTORE command * split test to functions and fix doc * change links to valkey * fix lint errors --------- Co-authored-by: Ubuntu --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 40 ++++++++ node/src/Commands.ts | 44 +++++++++ node/src/Transaction.ts | 28 ++++++ node/tests/RedisClusterClient.test.ts | 1 + node/tests/SharedTests.ts | 135 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 8 ++ 7 files changed, 257 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9491973e5..517f223e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added ZINTERSTORE command ([#1513](https://github.com/aws/glide-for-redis/pull/1513)) * Python: Added OBJECT ENCODING command ([#1471](https://github.com/aws/glide-for-redis/pull/1471)) * Python: Added OBJECT FREQ command ([#1472](https://github.com/aws/glide-for-redis/pull/1472)) * Python: Added OBJECT IDLETIME command ([#1474](https://github.com/aws/glide-for-redis/pull/1474)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 5bfadd5188..8a5476c0c4 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -11,7 +11,9 @@ import { import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { + AggregationType, ExpireOptions, + KeyWeight, RangeByIndex, RangeByLex, RangeByScore, @@ -82,6 +84,7 @@ import { createZAdd, createZCard, createZCount, + createZInterstore, createZPopMax, createZPopMin, createZRange, @@ -1882,6 +1885,43 @@ export class BaseClient { ); } + /** + * Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. + * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + * To get the result directly, see `zinter_withscores`. + * + * When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. + * + * See https://valkey.io/commands/zinterstore/ for more details. + * + * @param destination - The key of the destination sorted set. + * @param keys - The keys of the sorted sets with possible formats: + * string[] - for keys only. + * KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See `AggregationType`. + * @returns The number of elements in the resulting sorted set stored at `destination`. + * + * @example + * ```typescript + * // Example usage of zinterstore command with an existing key + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + * await client.zadd("key2", {"member1": 9.5}) + * await client.zinterstore("my_sorted_set", ["key1", "key2"]) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element. + * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 20} - "member1" is now stored in "my_sorted_set" with score of 20. + * await client.zinterstore("my_sorted_set", ["key1", "key2"] , AggregationType.MAX ) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element, and it's score is the maximum score between the sets. + * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5} - "member1" is now stored in "my_sorted_set" with score of 10.5. + * ``` + */ + public zinterstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): Promise { + return this.createWritePromise( + createZInterstore(destination, keys, aggregationType), + ); + } + /** Returns the length of the string value stored at `key`. * See https://redis.io/commands/strlen/ for more details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 0cddd74bbe..a4e898d3bf 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -827,6 +827,50 @@ export function createZAdd( return createCommand(RequestType.ZAdd, args); } +/** + * `KeyWeight` - pair of variables represents a weighted key for the `ZINTERSTORE` and `ZUNIONSTORE` sorted sets commands. + */ +export type KeyWeight = [string, number]; +/** + * `AggregationType` - representing aggregation types for `ZINTERSTORE` and `ZUNIONSTORE` sorted set commands. + */ +export type AggregationType = "SUM" | "MIN" | "MAX"; + +/** + * @internal + */ +export function createZInterstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, +): redis_request.Command { + const args = createZCmdStoreArgs(destination, keys, aggregationType); + return createCommand(RequestType.ZInterStore, args); +} + +function createZCmdStoreArgs( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, +): string[] { + const args: string[] = [destination, keys.length.toString()]; + + if (typeof keys[0] === "string") { + args.push(...(keys as string[])); + } else { + const weightsKeys = keys.map(([key]) => key); + args.push(...(weightsKeys as string[])); + const weights = keys.map(([, weight]) => weight.toString()); + args.push("WEIGHTS", ...weights); + } + + if (aggregationType) { + args.push("AGGREGATE", aggregationType); + } + + return args; +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index e5442e481a..c429743bfc 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -3,8 +3,10 @@ */ import { + AggregationType, ExpireOptions, InfoOptions, + KeyWeight, RangeByIndex, RangeByLex, RangeByScore, @@ -87,6 +89,7 @@ import { createZAdd, createZCard, createZCount, + createZInterstore, createZPopMax, createZPopMin, createZRange, @@ -1036,6 +1039,31 @@ export class BaseTransaction> { ); } + /** + * Computes the intersection of sorted sets given by the specified `keys` and stores the result in `destination`. + * If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created. + * + * When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. + * + * See https://valkey.io/commands/zinterstore/ for more details. + * + * @param destination - The key of the destination sorted set. + * @param keys - The keys of the sorted sets with possible formats: + * string[] - for keys only. + * KeyWeight[] - for weighted keys with score multipliers. + * @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See `AggregationType`. + * Command Response - The number of elements in the resulting sorted set stored at `destination`. + */ + public zinterstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): T { + return this.addAndReturn( + createZInterstore(destination, keys, aggregationType), + ); + } + /** Returns the string representation of the type of the value stored at `key`. * See https://redis.io/commands/type/ for more details. * diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 4ab7c49e1c..cbe5254b44 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -288,6 +288,7 @@ describe("RedisClusterClient", () => { client.smove("abc", "zxy", "value"), client.renamenx("abc", "zxy"), client.sinter(["abc", "zxy", "lkn"]), + client.zinterstore("abc", ["zxy", "lkn"]), // TODO all rest multi-key commands except ones tested below ]; diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 1844c8c51b..52b43589af 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1878,6 +1878,141 @@ export function runBaseTests(config: { config.timeout, ); + // Zinterstore command tests + async function zinterstoreWithAggregation(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 2.0, two: 3.0, three: 4.0 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Intersection results are aggregated by the MAX score of elements + expect(await client.zinterstore(key3, [key1, key2], "MAX")).toEqual(2); + const zinterstoreMapMax = await client.zrangeWithScores(key3, range); + const expectedMapMax = { + one: 2, + two: 3, + }; + expect(compareMaps(zinterstoreMapMax, expectedMapMax)).toBe(true); + + // Intersection results are aggregated by the MIN score of elements + expect(await client.zinterstore(key3, [key1, key2], "MIN")).toEqual(2); + const zinterstoreMapMin = await client.zrangeWithScores(key3, range); + const expectedMapMin = { + one: 1, + two: 2, + }; + expect(compareMaps(zinterstoreMapMin, expectedMapMin)).toBe(true); + + // Intersection results are aggregated by the SUM score of elements + expect(await client.zinterstore(key3, [key1, key2], "SUM")).toEqual(2); + const zinterstoreMapSum = await client.zrangeWithScores(key3, range); + const expectedMapSum = { + one: 3, + two: 5, + }; + expect(compareMaps(zinterstoreMapSum, expectedMapSum)).toBe(true); + } + + async function zinterstoreBasicTest(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 2.0, two: 3.0, three: 4.0 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + expect(await client.zinterstore(key3, [key1, key2])).toEqual(2); + const zinterstoreMap = await client.zrangeWithScores(key3, range); + const expectedMap = { + one: 3, + two: 5, + }; + expect(compareMaps(zinterstoreMap, expectedMap)).toBe(true); + } + + async function zinterstoreWithWeightsAndAggregation(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + const key3 = "{testKey}:3-" + uuidv4(); + const range = { + start: 0, + stop: -1, + }; + const membersScores1 = { one: 1.0, two: 2.0 }; + const membersScores2 = { one: 2.0, two: 3.0, three: 4.0 }; + + expect(await client.zadd(key1, membersScores1)).toEqual(2); + expect(await client.zadd(key2, membersScores2)).toEqual(3); + + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + expect( + await client.zinterstore( + key3, + [ + [key1, 2.0], + [key2, 2.0], + ], + "SUM", + ), + ).toEqual(2); + const zinterstoreMapMultiplied = await client.zrangeWithScores( + key3, + range, + ); + const expectedMapMultiplied = { + one: 6, + two: 10, + }; + expect( + compareMaps(zinterstoreMapMultiplied, expectedMapMultiplied), + ).toBe(true); + } + + async function zinterstoreEmptyCases(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + // Non existing key + expect( + await client.zinterstore(key2, [ + key1, + "{testKey}-non_existing_key", + ]), + ).toEqual(0); + + // Empty list check + await expect(client.zinterstore("{xyz}", [])).rejects.toThrow(); + } + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinterstore test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + await zinterstoreBasicTest(client); + await zinterstoreWithAggregation(client); + await zinterstoreWithWeightsAndAggregation(client); + await zinterstoreEmptyCases(client); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `type test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 6daeb88fe7..e39831c193 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -229,6 +229,8 @@ export async function transactionTest( const key9 = "{key}" + uuidv4(); const key10 = "{key}" + uuidv4(); const key11 = "{key}" + uuidv4(); // hyper log log + const key12 = "{key}" + uuidv4(); + const key13 = "{key}" + uuidv4(); const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; @@ -349,6 +351,12 @@ export async function transactionTest( args.push(["member2", "member3", "member4", "member5"]); baseTransaction.zrangeWithScores(key8, { start: 0, stop: -1 }); args.push({ member2: 3, member3: 3.5, member4: 4, member5: 5 }); + baseTransaction.zadd(key12, { one: 1, two: 2 }); + args.push(2); + baseTransaction.zadd(key13, { one: 1, two: 2, tree: 3.5 }); + args.push(3); + baseTransaction.zinterstore(key12, [key12, key13]); + args.push(2); baseTransaction.zcount(key8, { value: 2 }, "positiveInfinity"); args.push(4); baseTransaction.zpopmin(key8); From ddc1425f2ffd3b5c65435f89cb1f6536f83bfa58 Mon Sep 17 00:00:00 2001 From: Gilboab <97948000+GilboaAWS@users.noreply.github.com> Date: Sun, 9 Jun 2024 18:07:56 +0300 Subject: [PATCH 4/5] Python: added LMOVE and BLMOVE commands (#1536) Python: added LMOVE and BLMOVE commands Co-authored-by: Shoham Elias --- CHANGELOG.md | 1 + python/python/glide/__init__.py | 3 +- .../glide/async_commands/command_args.py | 16 ++ python/python/glide/async_commands/core.py | 97 +++++++++- .../glide/async_commands/transaction.py | 66 ++++++- python/python/tests/test_async_client.py | 165 +++++++++++++++++- python/python/tests/test_transaction.py | 6 +- 7 files changed, 348 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 517f223e97..cccb3c3026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * Python: Added SINTERCARD command ([#1511](https://github.com/aws/glide-for-redis/pull/1511)) * Python: Added SORT command ([#1439](https://github.com/aws/glide-for-redis/pull/1439)) * Node: Added OBJECT ENCODING command ([#1518](https://github.com/aws/glide-for-redis/pull/1518)) +* Python: Added LMOVE and BLMOVE commands ([#1536](https://github.com/aws/glide-for-redis/pull/1536)) ### Breaking Changes * Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494)) diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index ebd86eca1c..ae938e68d6 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -1,6 +1,6 @@ # Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 -from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, @@ -98,6 +98,7 @@ "json", "LexBoundary", "Limit", + "ListDirection", "RangeByIndex", "RangeByLex", "RangeByScore", diff --git a/python/python/glide/async_commands/command_args.py b/python/python/glide/async_commands/command_args.py index d308ca9ed7..b2e379bff8 100644 --- a/python/python/glide/async_commands/command_args.py +++ b/python/python/glide/async_commands/command_args.py @@ -43,3 +43,19 @@ class OrderBy(Enum): """ DESC: Sort in descending order. """ + + +class ListDirection(Enum): + """ + Enumeration representing element popping or adding direction for List commands. + """ + + LEFT = "LEFT" + """ + LEFT: Represents the option that elements should be popped from or added to the left side of a list. + """ + + RIGHT = "RIGHT" + """ + RIGHT: Represents the option that elements should be popped from or added to the right side of a list. + """ diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 57d41fbb17..b1284c9b08 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -16,7 +16,7 @@ get_args, ) -from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.sorted_set import ( AggregationType, InfBound, @@ -1496,6 +1496,101 @@ async def linsert( ), ) + async def lmove( + self, + source: str, + destination: str, + where_from: ListDirection, + where_to: ListDirection, + ) -> Optional[str]: + """ + Atomically pops and removes the left/right-most element to the list stored at `source` + depending on `where_from`, and pushes the element at the first/last element of the list + stored at `destination` depending on `where_to`. + + When in cluster mode, both `source` and `destination` must map to the same hash slot. + + See https://valkey.io/commands/lmove/ for details. + + Args: + source (str): The key to the source list. + destination (str): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + + Returns: + Optional[str]: The popped element, or None if `source` does not exist. + + Examples: + >>> client.lpush("testKey1", ["two", "one"]) + >>> client.lpush("testKey2", ["four", "three"]) + >>> await client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT) + "one" + >>> updated_array1 = await client.lrange("testKey1", 0, -1) + ["two"] + >>> await client.lrange("testKey2", 0, -1) + ["one", "three", "four"] + + Since: Redis version 6.2.0. + """ + return cast( + Optional[str], + await self._execute_command( + RequestType.LMove, + [source, destination, where_from.value, where_to.value], + ), + ) + + async def blmove( + self, + source: str, + destination: str, + where_from: ListDirection, + where_to: ListDirection, + timeout: float, + ) -> Optional[str]: + """ + Blocks the connection until it pops atomically and removes the left/right-most element to the + list stored at `source` depending on `where_from`, and pushes the element at the first/last element + of the list stored at `destination` depending on `where_to`. + `BLMOVE` is the blocking variant of `LMOVE`. + + Notes: + 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. + 2. `BLMOVE` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices. + + See https://valkey.io/commands/blmove/ for details. + + Args: + source (str): The key to the source list. + destination (str): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + + Returns: + Optional[str]: The popped element, or None if `source` does not exist or if the operation timed-out. + + Examples: + >>> await client.lpush("testKey1", ["two", "one"]) + >>> await client.lpush("testKey2", ["four", "three"]) + >>> await client.blmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT, 0.1) + "one" + >>> await client.lrange("testKey1", 0, -1) + ["two"] + >>> updated_array2 = await client.lrange("testKey2", 0, -1) + ["one", "three", "four"] + + Since: Redis version 6.2.0. + """ + return cast( + Optional[str], + await self._execute_command( + RequestType.BLMove, + [source, destination, where_from.value, where_to.value, str(timeout)], + ), + ) + async def sadd(self, key: str, members: List[str]) -> int: """ Add specified members to the set stored at `key`. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index c33497e7ed..c94a4d0b35 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -3,7 +3,7 @@ import threading from typing import List, Mapping, Optional, Tuple, TypeVar, Union -from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, @@ -931,7 +931,7 @@ def linsert( """ Inserts `element` in the list at `key` either before or after the `pivot`. - See https://redis.io/commands/linsert/ for details. + See https://valkey.io/commands/linsert/ for details. Args: key (str): The key of the list. @@ -949,6 +949,68 @@ def linsert( RequestType.LInsert, [key, position.value, pivot, element] ) + def lmove( + self: TTransaction, + source: str, + destination: str, + where_from: ListDirection, + where_to: ListDirection, + ) -> TTransaction: + """ + Atomically pops and removes the left/right-most element to the list stored at `source` + depending on `where_from`, and pushes the element at the first/last element of the list + stored at `destination` depending on `where_to`. + + See https://valkey.io/commands/lmove/ for details. + + Args: + source (str): The key to the source list. + destination (str): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + + Command response: + Optional[str]: The popped element, or `None` if `source` does not exist. + + Since: Redis version 6.2.0. + """ + return self.append_command( + RequestType.LMove, [source, destination, where_from.value, where_to.value] + ) + + def blmove( + self: TTransaction, + source: str, + destination: str, + where_from: ListDirection, + where_to: ListDirection, + timeout: float, + ) -> TTransaction: + """ + Blocks the connection until it pops atomically and removes the left/right-most element to the + list stored at `source` depending on `where_from`, and pushes the element at the first/last element + of the list stored at `destination` depending on `where_to`. + `blmove` is the blocking variant of `lmove`. + + See https://valkey.io/commands/blmove/ for details. + + Args: + source (str): The key to the source list. + destination (str): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + + Command response: + Optional[str]: The popped element, or `None` if `source` does not exist or if the operation timed-out. + + Since: Redis version 6.2.0. + """ + return self.append_command( + RequestType.BLMove, + [source, destination, where_from.value, where_to.value, str(timeout)], + ) + def sadd(self: TTransaction, key: str, members: List[str]) -> TTransaction: """ Add specified members to the set stored at `key`. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 48abd147fa..164acd4559 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -11,7 +11,7 @@ import pytest from glide import ClosingError, RequestError, Script -from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, @@ -1063,6 +1063,165 @@ async def test_linsert(self, redis_client: TRedisClient): with pytest.raises(RequestError): await redis_client.linsert(key2, InsertPosition.AFTER, "p", "e") + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_lmove(self, redis_client: TRedisClient): + key1 = "{SameSlot}" + get_random_string(10) + key2 = "{SameSlot}" + get_random_string(10) + + # Initialize the lists + assert await redis_client.lpush(key1, ["2", "1"]) == 2 + assert await redis_client.lpush(key2, ["4", "3"]) == 2 + + # Move from LEFT to LEFT + assert ( + await redis_client.lmove(key1, key2, ListDirection.LEFT, ListDirection.LEFT) + == "1" + ) + assert await redis_client.lrange(key1, 0, -1) == ["2"] + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"] + + # Move from LEFT to RIGHT + assert ( + await redis_client.lmove( + key1, key2, ListDirection.LEFT, ListDirection.RIGHT + ) + == "2" + ) + assert await redis_client.lrange(key1, 0, -1) == [] + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4", "2"] + + # Move from RIGHT to LEFT - non-existing destination key + assert ( + await redis_client.lmove( + key2, key1, ListDirection.RIGHT, ListDirection.LEFT + ) + == "2" + ) + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"] + assert await redis_client.lrange(key1, 0, -1) == ["2"] + + # Move from RIGHT to RIGHT + assert ( + await redis_client.lmove( + key2, key1, ListDirection.RIGHT, ListDirection.RIGHT + ) + == "4" + ) + assert await redis_client.lrange(key2, 0, -1) == ["1", "3"] + assert await redis_client.lrange(key1, 0, -1) == ["2", "4"] + + # Non-existing source key + assert ( + await redis_client.lmove( + "{SameSlot}non_existing_key", + key1, + ListDirection.LEFT, + ListDirection.LEFT, + ) + is None + ) + + # Non-list source key + key3 = get_random_string(10) + assert await redis_client.set(key3, "value") == OK + with pytest.raises(RequestError): + await redis_client.lmove(key3, key1, ListDirection.LEFT, ListDirection.LEFT) + + # Non-list destination key + with pytest.raises(RequestError): + await redis_client.lmove(key1, key3, ListDirection.LEFT, ListDirection.LEFT) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_blmove(self, redis_client: TRedisClient): + key1 = "{SameSlot}" + get_random_string(10) + key2 = "{SameSlot}" + get_random_string(10) + + # Initialize the lists + assert await redis_client.lpush(key1, ["2", "1"]) == 2 + assert await redis_client.lpush(key2, ["4", "3"]) == 2 + + # Move from LEFT to LEFT with blocking + assert ( + await redis_client.blmove( + key1, key2, ListDirection.LEFT, ListDirection.LEFT, 0.1 + ) + == "1" + ) + assert await redis_client.lrange(key1, 0, -1) == ["2"] + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"] + + # Move from LEFT to RIGHT with blocking + assert ( + await redis_client.blmove( + key1, key2, ListDirection.LEFT, ListDirection.RIGHT, 0.1 + ) + == "2" + ) + assert await redis_client.lrange(key1, 0, -1) == [] + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4", "2"] + + # Move from RIGHT to LEFT non-existing destination with blocking + assert ( + await redis_client.blmove( + key2, key1, ListDirection.RIGHT, ListDirection.LEFT, 0.1 + ) + == "2" + ) + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"] + assert await redis_client.lrange(key1, 0, -1) == ["2"] + + # Move from RIGHT to RIGHT with blocking + assert ( + await redis_client.blmove( + key2, key1, ListDirection.RIGHT, ListDirection.RIGHT, 0.1 + ) + == "4" + ) + assert await redis_client.lrange(key2, 0, -1) == ["1", "3"] + assert await redis_client.lrange(key1, 0, -1) == ["2", "4"] + + # Non-existing source key with blocking + assert ( + await redis_client.blmove( + "{SameSlot}non_existing_key", + key1, + ListDirection.LEFT, + ListDirection.LEFT, + 0.1, + ) + is None + ) + + # Non-list source key with blocking + key3 = get_random_string(10) + assert await redis_client.set(key3, "value") == OK + with pytest.raises(RequestError): + await redis_client.blmove( + key3, key1, ListDirection.LEFT, ListDirection.LEFT, 0.1 + ) + + # Non-list destination key with blocking + with pytest.raises(RequestError): + await redis_client.blmove( + key1, key3, ListDirection.LEFT, ListDirection.LEFT, 0.1 + ) + + # BLMOVE is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to + # avoid having the test block forever + async def endless_blmove_call(): + await redis_client.blmove( + "{SameSlot}non_existing_key", + key2, + ListDirection.LEFT, + ListDirection.RIGHT, + 0, + ) + + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_blmove_call(), timeout=3) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_sadd_srem_smembers_scard(self, redis_client: TRedisClient): @@ -3933,6 +4092,10 @@ async def test_multi_key_command_returns_cross_slot_error( redis_client.zunion(["def", "ghi"]), redis_client.zunion_withscores(["def", "ghi"]), redis_client.sort_store("abc", "zxy"), + redis_client.lmove("abc", "zxy", ListDirection.LEFT, ListDirection.LEFT), + redis_client.blmove( + "abc", "zxy", ListDirection.LEFT, ListDirection.LEFT, 1 + ), ] if not await check_if_server_version_lt(redis_client, "7.0.0"): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 3ce0a3acf5..413ff597ff 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -6,7 +6,7 @@ import pytest from glide import RequestError -from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( GeospatialData, InsertPosition, @@ -182,6 +182,10 @@ async def transaction_test( args.append(OK) transaction.lrange(key5, 0, -1) args.append([value2, value]) + transaction.lmove(key5, key6, ListDirection.LEFT, ListDirection.LEFT) + args.append(value2) + transaction.blmove(key6, key5, ListDirection.LEFT, ListDirection.LEFT, 1) + args.append(value2) transaction.lpop_count(key5, 2) args.append([value2, value]) transaction.linsert(key5, InsertPosition.BEFORE, "non_existing_pivot", "element") From 9caf33d11f660bf2b13f41975b2f44530a3bcf3c Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Mon, 10 Jun 2024 16:33:51 +0000 Subject: [PATCH 5/5] Improved bitmap javadocs (#1530) Improved bitmap docs (#344) * Improved bitmap docs * Addressed PR comments * Update java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java * Update java/client/src/main/java/glide/api/models/BaseTransaction.java --------- Co-authored-by: Yury-Fridlyand --- .../api/commands/BitmapBaseCommands.java | 28 ++++++++++--------- .../glide/api/models/BaseTransaction.java | 28 ++++++++++--------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java index a22ad2ab3f..508cff39ca 100644 --- a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java @@ -12,6 +12,7 @@ import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** @@ -24,7 +25,7 @@ public interface BitmapBaseCommands { /** * Counts the number of set bits (population counting) in a string stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @return The number of set bits in the string. Returns zero if the key is missing as it is * treated as an empty string. @@ -44,7 +45,7 @@ public interface BitmapBaseCommands { * -1 being the last element of the list, -2 being the penultimate, and * so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @param start The starting offset byte index. * @param end The ending offset byte index. @@ -67,7 +68,7 @@ public interface BitmapBaseCommands { * so on. * * @since Redis 7.0 and above - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @param start The starting offset. * @param end The ending offset. @@ -92,7 +93,7 @@ public interface BitmapBaseCommands { * non-existent then the bit at offset is set to value and the preceding * bits are set to 0. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param offset The index of the bit to be set. * @param value The bit value to set at offset. The value must be 0 or @@ -110,7 +111,7 @@ public interface BitmapBaseCommands { * Returns the bit value at offset in the string value stored at key. * offset should be greater than or equal to zero. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param offset The index of the bit to return. * @return The bit at offset of the string. Returns zero if the key is empty or if the positive @@ -127,7 +128,7 @@ public interface BitmapBaseCommands { /** * Returns the position of the first bit matching the given bit value. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @return The position of the first occurrence matching bit in the binary value of @@ -150,7 +151,7 @@ public interface BitmapBaseCommands { * indicating offsets starting at the end of the list, with -1 being the last byte of * the list, -2 being the penultimate, and so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -174,7 +175,7 @@ public interface BitmapBaseCommands { * negative numbers indicating offsets starting at the end of the list, with -1 being * the last byte of the list, -2 being the penultimate, and so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -203,7 +204,7 @@ public interface BitmapBaseCommands { * list, -2 being the penultimate, and so on. * * @since Redis 7.0 and above. - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -230,7 +231,7 @@ CompletableFuture bitpos( * * @apiNote When in cluster mode, destination and all keys must map to * the same hash slot. - * @see redis.io for details. + * @see valkey.io for details. * @param bitwiseOperation The bitwise operation to perform. * @param destination The key that will store the resulting string. * @param keys The list of keys to perform the bitwise operation on. @@ -251,7 +252,7 @@ CompletableFuture bitop( * Reads or modifies the array of bits representing the string that is held at key * based on the specified subCommands. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param subCommands The subCommands to be performed on the binary value of the string at * key, which could be any of the following: @@ -289,10 +290,11 @@ CompletableFuture bitop( /** * Reads the array of bits representing the string that is held at key based on the - * specified subCommands. + * specified subCommands.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. * * @since Redis 6.0 and above - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param subCommands The GET subCommands to be performed. * @return An array of results from the GET subcommands. 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 1dfe7fadc1..ad04013935 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -216,6 +216,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; @@ -3500,7 +3501,7 @@ public T copy(@NonNull String source, @NonNull String destination) { /** * Counts the number of set bits (population counting) in a string stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @return Command Response - The number of set bits in the string. Returns zero if the key is * missing as it is treated as an empty string. @@ -3519,7 +3520,7 @@ public T bitcount(@NonNull String key) { * -1 being the last element of the list, -2 being the penultimate, and * so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @param start The starting byte offset. * @param end The ending byte offset. @@ -3543,7 +3544,7 @@ public T bitcount(@NonNull String key, long start, long end) { * so on. * * @since Redis 7.0 and above - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @param start The starting offset. * @param end The ending offset. @@ -3738,7 +3739,7 @@ public T functionList(@NonNull String libNamePattern, boolean withCode) { * non-existent then the bit at offset is set to value and the preceding * bits are set to 0. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param offset The index of the bit to be set. * @param value The bit value to set at offset. The value must be 0 or @@ -3755,7 +3756,7 @@ public T setbit(@NonNull String key, long offset, long value) { * Returns the bit value at offset in the string value stored at key. * offset should be greater than or equal to zero. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param offset The index of the bit to return. * @return Command Response - The bit at offset of the string. Returns zero if the key is empty or @@ -3837,7 +3838,7 @@ public T blmpop(@NonNull String[] keys, @NonNull ListDirection direction, double /** * Returns the position of the first bit matching the given bit value. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @return Command Response - The position of the first occurrence matching bit in @@ -3857,7 +3858,7 @@ public T bitpos(@NonNull String key, long bit) { * indicating offsets starting at the end of the list, with -1 being the last byte of * the list, -2 being the penultimate, and so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -3878,7 +3879,7 @@ public T bitpos(@NonNull String key, long bit, long start) { * negative numbers indicating offsets starting at the end of the list, with -1 being * the last byte of the list, -2 being the penultimate, and so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -3905,7 +3906,7 @@ public T bitpos(@NonNull String key, long bit, long start, long end) { * list, -2 being the penultimate, and so on. * * @since Redis 7.0 and above. - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -3934,7 +3935,7 @@ public T bitpos( * Perform a bitwise operation between multiple keys (containing string values) and store the * result in the destination. * - * @see redis.io for details. + * @see valkey.io for details. * @param bitwiseOperation The bitwise operation to perform. * @param destination The key that will store the resulting string. * @param keys The list of keys to perform the bitwise operation on. @@ -4147,7 +4148,7 @@ public T spopCount(@NonNull String key, long count) { * Reads or modifies the array of bits representing the string that is held at key * based on the specified subCommands. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param subCommands The subCommands to be performed on the binary value of the string at * key, which could be any of the following: @@ -4178,10 +4179,11 @@ public T bitfield(@NonNull String key, @NonNull BitFieldSubCommands[] subCommand /** * Reads the array of bits representing the string that is held at key based on the - * specified subCommands. + * specified subCommands.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. * * @since Redis 6.0 and above - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param subCommands The GET subCommands to be performed. * @return Command Response - An array of results from the GET subcommands.