diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 2ac0f2f9ed..67f03e3c5f 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -239,7 +239,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { } b"INCRBYFLOAT" | b"HINCRBYFLOAT" => Some(ExpectedReturnType::Double), b"HEXISTS" | b"HSETNX" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT" - | b"SISMEMBER" | b"PERSIST" | b"SMOVE" => Some(ExpectedReturnType::Boolean), + | b"SISMEMBER" | b"PERSIST" | b"SMOVE" | b"RENAMENX" => Some(ExpectedReturnType::Boolean), b"SMISMEMBER" => Some(ExpectedReturnType::ArrayOfBools), b"SMEMBERS" | b"SINTER" => Some(ExpectedReturnType::Set), b"ZSCORE" => Some(ExpectedReturnType::DoubleOrNull), diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index d86eeb0445..a45f273ce0 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -163,6 +163,7 @@ enum RequestType { GeoAdd = 121; GeoHash = 122; ObjectEncoding = 123; + RenameNx = 130; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index edf7c27dbe..28d46d1485 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -131,6 +131,7 @@ pub enum RequestType { GeoAdd = 121, GeoHash = 122, ObjectEncoding = 123, + RenameNx = 130, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -265,6 +266,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::GeoAdd => RequestType::GeoAdd, ProtobufRequestType::GeoHash => RequestType::GeoHash, ProtobufRequestType::ObjectEncoding => RequestType::ObjectEncoding, + ProtobufRequestType::RenameNx => RequestType::RenameNx, } } } @@ -395,6 +397,7 @@ impl RequestType { RequestType::GeoAdd => Some(cmd("GEOADD")), RequestType::GeoHash => Some(cmd("GEOHASH")), RequestType::ObjectEncoding => Some(get_two_word_command("OBJECT", "ENCODING")), + RequestType::RenameNx => Some(cmd("RENAMENX")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index f9812dd4fb..0862d3cc3b 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -52,6 +52,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.RPop; import static redis_request.RedisRequestOuterClass.RequestType.RPush; import static redis_request.RedisRequestOuterClass.RequestType.RPushX; +import static redis_request.RedisRequestOuterClass.RequestType.RenameNx; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; @@ -341,6 +342,12 @@ public CompletableFuture objectEncoding(@NonNull String key) { ObjectEncoding, new String[] {key}, this::handleStringOrNullResponse); } + @Override + public CompletableFuture renamenx(@NonNull String key, @NonNull String newKey) { + return commandManager.submitNewCommand( + RenameNx, new String[] {key, newKey}, this::handleBooleanResponse); + } + @Override public CompletableFuture incr(@NonNull String key) { return commandManager.submitNewCommand(Incr, new String[] {key}, this::handleLongResponse); diff --git a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java index ede7fede20..2ad2446626 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -389,4 +389,23 @@ CompletableFuture pexpireAt( * } */ CompletableFuture objectEncoding(String key); + + /** + * Renames key to newKey if newKey does not yet exist. + * + * @apiNote When in cluster mode, both key and newKey must map to the + * same hash slot + * . + * @see redis.io for details. + * @param key The key to rename. + * @param newKey The new key name. + * @return true if key was renamed to newKey, false + * if newKey already exists. + * @example + *
{@code
+     * Boolean renamed = client.renamenx("old_key", "new_key").get();
+     * assert renamed;
+     * }
+ */ + CompletableFuture renamenx(String key, String newKey); } 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 16be2fd746..22c8af00aa 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -64,6 +64,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.RPop; import static redis_request.RedisRequestOuterClass.RequestType.RPush; import static redis_request.RedisRequestOuterClass.RequestType.RPushX; +import static redis_request.RedisRequestOuterClass.RequestType.RenameNx; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; @@ -1975,6 +1976,21 @@ public T type(@NonNull String key) { return getThis(); } + /** + * Renames key to newKey if newKey does not yet exist. + * + * @see redis.io for details. + * @param key The key to rename. + * @param newKey The new key name. + * @return Command Response - true if key was renamed to newKey + * , false if newKey already exists. + */ + public T renamenx(@NonNull String key, @NonNull String newKey) { + ArgsArray commandArgs = buildArgs(key, newKey); + protobufTransaction.addCommands(buildCommand(RenameNx, commandArgs)); + return getThis(); + } + /** * Inserts element in the list at key either before or after the * pivot. diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 46ad50f281..c068ad1251 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -84,6 +84,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.RPop; import static redis_request.RedisRequestOuterClass.RequestType.RPush; import static redis_request.RedisRequestOuterClass.RequestType.RPushX; +import static redis_request.RedisRequestOuterClass.RequestType.RenameNx; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; @@ -3024,6 +3025,29 @@ public void type_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void renamenx_returns_success() { + // setup + String key = "key1"; + String newKey = "key2"; + String[] arguments = new String[] {key, newKey}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RenameNx), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.renamenx(key, newKey); + + // verify + assertEquals(testResponse, response); + assertTrue(response.get()); + } + @SneakyThrows @Test public void time_returns_success() { 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 63adc075b8..e047b53b37 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -69,6 +69,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.RPop; import static redis_request.RedisRequestOuterClass.RequestType.RPush; import static redis_request.RedisRequestOuterClass.RequestType.RPushX; +import static redis_request.RedisRequestOuterClass.RequestType.RenameNx; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; import static redis_request.RedisRequestOuterClass.RequestType.SCard; import static redis_request.RedisRequestOuterClass.RequestType.SDiffStore; @@ -462,6 +463,9 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.type("key"); results.add(Pair.of(Type, buildArgs("key"))); + transaction.renamenx("key", "newKey"); + results.add(Pair.of(RenameNx, buildArgs("key", "newKey"))); + transaction.linsert("key", AFTER, "pivot", "elem"); results.add(Pair.of(LInsert, buildArgs("key", "AFTER", "pivot", "elem"))); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 9b1a36ecc0..1ca873fafc 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -975,6 +975,48 @@ public void smove(BaseClient client) { } } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void renamenx(BaseClient client) { + String key1 = "{key}" + UUID.randomUUID(); + String key2 = "{key}" + UUID.randomUUID(); + String key3 = "{key}" + UUID.randomUUID(); + + assertEquals(OK, client.set(key3, "key3").get()); + + // rename missing key + var executionException = + assertThrows(ExecutionException.class, () -> client.renamenx(key1, key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().toLowerCase().contains("no such key")); + + // rename a string + assertEquals(OK, client.set(key1, "key1").get()); + assertTrue(client.renamenx(key1, key2).get()); + assertFalse(client.renamenx(key2, key3).get()); + assertEquals("key1", client.get(key2).get()); + assertEquals(1, client.del(new String[] {key1, key2}).get()); + + // rename a set + assertEquals(3, client.sadd(key1, new String[] {"a", "b", "c"}).get()); + assertTrue(client.renamenx(key1, key2).get()); + assertFalse(client.renamenx(key2, key3).get()); + assertEquals(Set.of("a", "b", "c"), client.smembers(key2).get()); + assertEquals("none", client.type(key1).get()); + + // this one remains unchanged + assertEquals("key3", client.get(key3).get()); + + // same-slot requirement + if (client instanceof RedisClusterClient) { + executionException = + assertThrows(ExecutionException.class, () -> client.renamenx("abc", "zxy").get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().toLowerCase().contains("crossslot")); + } + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index c2ce0f9a7f..619c5b0450 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -50,6 +50,7 @@ public static BaseTransaction transactionTest(BaseTransaction baseTransact baseTransaction.set(key2, value2, SetOptions.builder().returnOldValue(true).build()); baseTransaction.strlen(key2); baseTransaction.customCommand(new String[] {"MGET", key1, key2}); + baseTransaction.renamenx(key1, key2); baseTransaction.exists(new String[] {key1}); baseTransaction.persist(key1); @@ -177,6 +178,7 @@ public static Object[] transactionTestResult() { null, (long) value1.length(), // strlen(key2) new String[] {value1, value2}, + false, // renamenx(key1, key2) 1L, Boolean.FALSE, // persist(key1) 1L,