Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python: add COPY Command #1626

Merged
merged 4 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
* Python: Added ZREVRANK command ([#1614](https://github.com/aws/glide-for-redis/pull/1614))
* Python: Added XDEL command ([#1619](https://github.com/aws/glide-for-redis/pull/1619))
* Python: Added XRANGE command ([#1624](https://github.com/aws/glide-for-redis/pull/1624))
* Python: Added COPY command ([#1626](https://github.com/aws/glide-for-redis/pull/1626))

### Breaking Changes
* Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494))
Expand Down
40 changes: 40 additions & 0 deletions python/python/glide/async_commands/cluster_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,43 @@ async def flushall(
TClusterResponse[TOK],
await self._execute_command(RequestType.FlushAll, args, route),
)

async def copy(
self,
source: str,
destination: str,
replace: Optional[bool] = None,
) -> bool:
"""
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.

See https://valkey.io/commands/copy for more details.

Note:
Both `source` and `destination` must map to the same hash slot.

Args:
source (str): The key to the source value.
destination (str): The key where the value should be copied to.
replace (Optional[bool]): If the destination key should be removed before copying the value to it.

Returns:
bool: True if the source was copied. Otherwise, returns False.

Examples:
>>> await client.set("source", "sheep")
>>> await client.copy("source", "destination")
True # Source was copied
>>> await client.get("destination")
"sheep"

Since: Redis version 6.2.0.
"""
args = [source, destination]
if replace is True:
args.append("REPLACE")
return cast(
bool,
await self._execute_command(RequestType.Copy, args),
)
44 changes: 44 additions & 0 deletions python/python/glide/async_commands/standalone_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,47 @@ async def flushall(self, flush_mode: Optional[FlushMode] = None) -> TOK:
TOK,
await self._execute_command(RequestType.FlushAll, args),
)

async def copy(
self,
source: str,
destination: str,
destinationDB: Optional[int] = None,
replace: Optional[bool] = None,
) -> bool:
"""
Copies the value stored at the `source` to the `destination` key. If `destinationDB`
is specified, the value will be copied to the database specified by `destinationDB`,
otherwise the current database will be used. When `replace` is True, removes the
`destination` key first if it already exists, otherwise performs no action.

See https://valkey.io/commands/copy for more details.

Args:
source (str): The key to the source value.
destination (str): The key where the value should be copied to.
destinationDB (Optional[int]): The alternative logical database index for the destination key.
replace (Optional[bool]): If the destination key should be removed before copying the value to it.

Returns:
bool: True if the source was copied. Otherwise, return False.

Examples:
>>> await client.set("source", "sheep")
>>> await client.copy("source", "destination", 1, False)
True # Source was copied
>>> await client.select(1)
>>> await client.get("destination")
"sheep"

Since: Redis version 6.2.0.
"""
args = [source, destination]
if destinationDB is not None:
args.extend(["DB", str(destinationDB)])
if replace is True:
args.append("REPLACE")
return cast(
bool,
await self._execute_command(RequestType.Copy, args),
)
62 changes: 62 additions & 0 deletions python/python/glide/async_commands/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -3627,6 +3627,40 @@ def sort_store(
)
return self.append_command(RequestType.Sort, args)

def copy(
self: TTransaction,
source: str,
destination: str,
destinationDB: Optional[int] = None,
replace: Optional[bool] = None,
) -> TTransaction:
"""
Copies the value stored at the `source` to the `destination` key. If `destinationDB`
is specified, the value will be copied to the database specified by `destinationDB`,
otherwise the current database will be used. When `replace` is True, removes the
`destination` key first if it already exists, otherwise performs no action.

See https://valkey.io/commands/copy for more details.

Args:
source (str): The key to the source value.
destination (str): The key where the value should be copied to.
destinationDB (Optional[int]): The alternative logical database index for the destination key.
replace (Optional[bool]): If the destination key should be removed before copying the value to it.

Command response:
bool: True if the source was copied. Otherwise, return False.

Since: Redis version 6.2.0.
"""
args = [source, destination]
if destinationDB is not None:
args.extend(["DB", str(destinationDB)])
if replace is not None:
args.append("REPLACE")

return self.append_command(RequestType.Copy, args)


class ClusterTransaction(BaseTransaction):
"""
Expand Down Expand Up @@ -3694,4 +3728,32 @@ def sort_store(
args = _build_sort_args(key, None, limit, None, order, alpha, store=destination)
return self.append_command(RequestType.Sort, args)

def copy(
self: TTransaction,
source: str,
destination: str,
replace: Optional[bool] = None,
) -> TTransaction:
"""
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.

See https://valkey.io/commands/copy for more details.

Args:
source (str): The key to the source value.
destination (str): The key where the value should be copied to.
replace (Optional[bool]): If the destination key should be removed before copying the value to it.

Command response:
bool: True if the source was copied. Otherwise, return False.

Since: Redis version 6.2.0.
"""
args = [source, destination]
if replace is not None:
args.append("REPLACE")

return self.append_command(RequestType.Copy, args)

# TODO: add all CLUSTER commands
104 changes: 103 additions & 1 deletion python/python/tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5632,6 +5632,107 @@ async def test_getex(self, redis_client: TRedisClient):
)
assert await redis_client.ttl(key1) == -1

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_copy_no_database(self, redis_client: TRedisClient):
min_version = "6.2.0"
if await check_if_server_version_lt(redis_client, min_version):
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")

source = f"{{testKey}}:1-{get_random_string(10)}"
destination = f"{{testKey}}:2-{get_random_string(10)}"
value1 = get_random_string(5)
value2 = get_random_string(5)

# neither key exists
assert await redis_client.copy(source, destination, replace=False) is False
assert await redis_client.copy(source, destination) is False

# source exists, destination does not
await redis_client.set(source, value1)
assert await redis_client.copy(source, destination, replace=False) is True
assert await redis_client.get(destination) == value1

# new value for source key
await redis_client.set(source, value2)

# both exists, no REPLACE
assert await redis_client.copy(source, destination) is False
assert await redis_client.copy(source, destination, replace=False) is False
assert await redis_client.get(destination) == value1

# both exists, with REPLACE
assert await redis_client.copy(source, destination, replace=True) is True
assert await redis_client.get(destination) == value2

@pytest.mark.parametrize("cluster_mode", [False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_copy_database(self, redis_client: RedisClient):
min_version = "6.2.0"
if await check_if_server_version_lt(redis_client, min_version):
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")

source = get_random_string(10)
destination = get_random_string(10)
value1 = get_random_string(5)
value2 = get_random_string(5)
index0 = 0
index1 = 1
index2 = 2

try:
assert await redis_client.select(index0) == OK

# neither key exists
assert (
await redis_client.copy(source, destination, index1, replace=False)
is False
)

# source exists, destination does not
await redis_client.set(source, value1)
assert (
await redis_client.copy(source, destination, index1, replace=False)
is True
)
assert await redis_client.select(index1) == OK
assert await redis_client.get(destination) == value1

# new value for source key
assert await redis_client.select(index0) == OK
await redis_client.set(source, value2)

# no REPLACE, copying to existing key on DB 0 & 1, non-existing key on DB 2
assert (
await redis_client.copy(source, destination, index1, replace=False)
is False
)
assert (
await redis_client.copy(source, destination, index2, replace=False)
is True
)

# new value only gets copied to DB 2
assert await redis_client.select(index1) == OK
assert await redis_client.get(destination) == value1
assert await redis_client.select(index2) == OK
assert await redis_client.get(destination) == value2

# both exists, with REPLACE, when value isn't the same, source always get copied to destination
assert await redis_client.select(index0) == OK
assert (
await redis_client.copy(source, destination, index1, replace=True)
is True
)
assert await redis_client.select(index1) == OK
assert await redis_client.get(destination) == value2

# invalid DB index
with pytest.raises(RequestError):
await redis_client.copy(source, destination, -1, replace=True)
finally:
assert await redis_client.select(0) == OK


class TestMultiKeyCommandCrossSlot:
@pytest.mark.parametrize("cluster_mode", [True])
Expand Down Expand Up @@ -5682,7 +5783,8 @@ async def test_multi_key_command_returns_cross_slot_error(
"zxy",
GeospatialData(15, 37),
GeoSearchByBox(400, 400, GeoUnit.KILOMETERS),
)
),
redis_client.copy("abc", "zxy", replace=True),
]
)

Expand Down
25 changes: 25 additions & 0 deletions python/python/tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ async def transaction_test(
transaction.pexpiretime(key)
args.append(-1)

if not await check_if_server_version_lt(redis_client, "6.2.0"):
transaction.copy(key, key2, replace=True)
args.append(True)

transaction.rename(key, key2)
args.append(OK)

Expand Down Expand Up @@ -697,6 +701,27 @@ def test_transaction_clear(self):
transaction.clear()
assert len(transaction.commands) == 0

@pytest.mark.parametrize("cluster_mode", [False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_standalone_copy_transaction(self, redis_client: RedisClient):
min_version = "6.2.0"
if await check_if_server_version_lt(redis_client, min_version):
return pytest.mark.skip(reason=f"Redis version required >= {min_version}")

keyslot = get_random_string(3)
key = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot
key1 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot
value = get_random_string(5)
transaction = Transaction()
transaction.select(1)
transaction.set(key, value)
transaction.copy(key, key1, 1, replace=True)
transaction.get(key1)
result = await redis_client.exec(transaction)
assert result is not None
assert result[2] == True
assert result[3] == value

@pytest.mark.parametrize("cluster_mode", [True, False])
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
async def test_transaction_chaining_calls(self, redis_client: TRedisClient):
Expand Down
Loading