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); + } }