diff --git a/CHANGELOG.md b/CHANGELOG.md index 73cbf542fe..dd61dec1d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Python: Added OBJECT IDLETIME command ([#1474](https://github.com/aws/glide-for-redis/pull/1474)) * Node: Added RENAMENX command ([#1483](https://github.com/aws/glide-for-redis/pull/1483)) * Python: Added OBJECT REFCOUNT command ([#1485](https://github.com/aws/glide-for-redis/pull/1485)) +* Python: Added RENAMENX command ([#1492](https://github.com/aws/glide-for-redis/pull/1492)) ## 0.4.0 (2024-05-26) diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 5d37e80318..d76b0a2c94 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -511,6 +511,31 @@ async def rename(self, key: str, new_key: str) -> TOK: TOK, await self._execute_command(RequestType.Rename, [key, new_key]) ) + async def renamenx(self, key: str, new_key: str) -> bool: + """ + Renames `key` to `new_key` if `new_key` does not yet exist. + + See https://valkey.io/commands/renamenx for more details. + + Note: + When in cluster mode, both `key` and `new_key` must map to the same hash slot. + + Args: + key (str): The key to rename. + new_key (str): The new key name. + + Returns: + bool: True if `key` was renamed to `new_key`, or False if `new_key` already exists. + + Examples: + >>> await client.renamenx("old_key", "new_key") + True # "old_key" was renamed to "new_key" + """ + return cast( + bool, + await self._execute_command(RequestType.RenameNX, [key, new_key]), + ) + async def delete(self, keys: List[str]) -> int: """ Delete one or more keys from the database. A key is ignored if it does not exist. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 4d00ac4375..b4b8f21c8f 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -154,6 +154,21 @@ def rename(self: TTransaction, key: str, new_key: str) -> TTransaction: """ return self.append_command(RequestType.Rename, [key, new_key]) + def renamenx(self: TTransaction, key: str, new_key: str) -> TTransaction: + """ + Renames `key` to `new_key` if `new_key` does not yet exist. + + See https://valkey.io/commands/renamenx for more details. + + Args: + key (str): The key to rename. + new_key (str): The new key name. + + Command response: + bool: True if `key` was renamed to `new_key`, or False if `new_key` already exists. + """ + return self.append_command(RequestType.RenameNX, [key, new_key]) + def custom_command(self: TTransaction, command_args: List[str]) -> TTransaction: """ Executes a single command, without checking inputs. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 88881bfb7c..bb6ab600d7 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -1457,6 +1457,30 @@ async def test_rename(self, redis_client: TRedisClient): "{same_slot}" + "non_existing_key", "{same_slot}" + "_rename" ) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_renamenx(self, redis_client: TRedisClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + key3 = f"{{testKey}}:3-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + # Verify that attempting to rename a non-existing key throws an error + with pytest.raises(RequestError): + assert await redis_client.renamenx(non_existing_key, key1) + + # Test RENAMENX with string values + assert await redis_client.set(key1, "key1") == OK + assert await redis_client.set(key3, "key3") == OK + # Test that RENAMENX can rename key1 to key2 (where key2 does not yet exist) + assert await redis_client.renamenx(key1, key2) is True + # Verify that key2 now holds the value that was in key1 + assert await redis_client.get(key2) == "key1" + # Verify that RENAMENX doesn't rename key2 to key3, since key3 already exists + assert await redis_client.renamenx(key2, key3) is False + # Verify that key3 remains unchanged + assert await redis_client.get(key3) == "key3" + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_exists(self, redis_client: TRedisClient): @@ -3462,6 +3486,7 @@ async def test_multi_key_command_returns_cross_slot_error( redis_client.sinterstore("abc", ["zxy", "lkn"]), redis_client.sdiff(["abc", "zxy", "lkn"]), redis_client.sdiffstore("abc", ["def", "ghi"]), + redis_client.renamenx("abc", "def"), ] 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 160ed6f0f6..da87ff9600 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -95,6 +95,9 @@ async def transaction_test( transaction.mget([key, key2]) args.append([value, value2]) + transaction.renamenx(key, key2) + args.append(False) + transaction.incr(key3) args.append(1) transaction.incrby(key3, 2)