From 7e32e1b599387022834cc740a4db9157485b1e64 Mon Sep 17 00:00:00 2001 From: Yi-Pin Chen Date: Fri, 21 Jun 2024 17:31:17 -0700 Subject: [PATCH 01/17] Python: add COPY Command (#1626) * Python: Added COPY command (#383) Python: Added COPY command * Updated CHANGELOG.md * Addressed review comments --- CHANGELOG.md | 1 + .../glide/async_commands/cluster_commands.py | 40 +++++++ .../async_commands/standalone_commands.py | 44 ++++++++ .../glide/async_commands/transaction.py | 62 +++++++++++ python/python/tests/test_async_client.py | 104 +++++++++++++++++- python/python/tests/test_transaction.py | 25 +++++ 6 files changed, 275 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ddac06130..807b09c548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/python/glide/async_commands/cluster_commands.py index 50fd8390f5..65957bc5f7 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/python/glide/async_commands/cluster_commands.py @@ -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), + ) diff --git a/python/python/glide/async_commands/standalone_commands.py b/python/python/glide/async_commands/standalone_commands.py index add1cdde1d..dbab238b60 100644 --- a/python/python/glide/async_commands/standalone_commands.py +++ b/python/python/glide/async_commands/standalone_commands.py @@ -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), + ) diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 4e45c99158..adced407f8 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -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): """ @@ -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 diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index b69f77090c..cdaa33f8b7 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -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]) @@ -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), ] ) diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 13702c4dc5..e4b86b1e83 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -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) @@ -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): From 2438d8b5b5e39985b0bc98a33a778c05709d8419 Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Sat, 22 Jun 2024 00:53:20 +0000 Subject: [PATCH 02/17] Java: Add `SORT` and `SORT_RO` commands (#1611) * Java: Add `SORT` and `SORT_RO` commands (#363) Co-authored-by: Yury-Fridlyand --- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + .../src/main/java/glide/api/BaseClient.java | 26 +++ .../src/main/java/glide/api/RedisClient.java | 31 +++ .../java/glide/api/RedisClusterClient.java | 34 +++ .../api/commands/GenericBaseCommands.java | 60 ++++++ .../api/commands/GenericClusterCommands.java | 72 +++++++ .../glide/api/commands/GenericCommands.java | 72 +++++++ .../glide/api/models/BaseTransaction.java | 54 +++++ .../glide/api/models/ClusterTransaction.java | 73 +++++++ .../java/glide/api/models/Transaction.java | 60 ++++++ .../api/models/commands/SortBaseOptions.java | 109 ++++++++++ .../models/commands/SortClusterOptions.java | 13 ++ .../api/models/commands/SortOptions.java | 77 +++++++ .../test/java/glide/api/RedisClientTest.java | 136 ++++++++++++ .../glide/api/RedisClusterClientTest.java | 199 ++++++++++++++++++ .../api/models/ClusterTransactionTests.java | 94 +++++++++ .../models/StandaloneTransactionTests.java | 143 +++++++++++++ .../glide/api/models/TransactionTests.java | 10 + .../test/java/glide/SharedCommandTests.java | 33 +++ .../java/glide/TransactionTestUtilities.java | 16 +- .../cluster/ClusterTransactionTests.java | 39 ++++ .../test/java/glide/cluster/CommandTests.java | 96 ++++++++- .../java/glide/standalone/CommandTests.java | 180 ++++++++++++++++ .../glide/standalone/TransactionTests.java | 79 +++++++ 25 files changed, 1707 insertions(+), 3 deletions(-) create mode 100644 java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java create mode 100644 java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java create mode 100644 java/client/src/main/java/glide/api/models/commands/SortOptions.java create mode 100644 java/client/src/test/java/glide/api/models/ClusterTransactionTests.java diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index a7fd57ab1b..12341ea919 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -233,6 +233,7 @@ enum RequestType { GetEx = 192; Dump = 193; Restore = 194; + SortReadOnly = 195; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 3e3e917ce6..526c14a2f4 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -203,6 +203,7 @@ pub enum RequestType { GetEx = 192, Dump = 193, Restore = 194, + SortReadOnly = 195, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -409,6 +410,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::GetEx => RequestType::GetEx, ProtobufRequestType::Dump => RequestType::Dump, ProtobufRequestType::Restore => RequestType::Restore, + ProtobufRequestType::SortReadOnly => RequestType::SortReadOnly, } } } @@ -613,6 +615,7 @@ impl RequestType { RequestType::GetEx => Some(cmd("GETEX")), RequestType::Dump => Some(cmd("DUMP")), RequestType::Restore => Some(cmd("RESTORE")), + RequestType::SortReadOnly => Some(cmd("SORT_RO")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 4b89a72ff6..7167ba4bff 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -2,6 +2,8 @@ package glide.api; import static glide.api.models.GlideString.gs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; @@ -118,6 +120,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Set; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Touch; @@ -2080,4 +2084,26 @@ public CompletableFuture restore( GlideString[] arguments = restoreOptions.toArgs(key, ttl, value); return commandManager.submitNewCommand(Restore, arguments, this::handleStringResponse); } + + @Override + public CompletableFuture sort(@NonNull String key) { + return commandManager.submitNewCommand( + Sort, + new String[] {key}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly(@NonNull String key) { + return commandManager.submitNewCommand( + SortReadOnly, + new String[] {key}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortStore(@NonNull String key, @NonNull String destination) { + return commandManager.submitNewCommand( + Sort, new String[] {key, STORE_COMMAND_STRING, destination}, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 5722a071f6..3a748cc8de 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -1,6 +1,8 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; @@ -32,6 +34,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RandomKey; import static redis_request.RedisRequestOuterClass.RequestType.Select; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Time; import static redis_request.RedisRequestOuterClass.RequestType.UnWatch; @@ -43,6 +47,7 @@ import glide.api.models.Transaction; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.configuration.RedisClientConfiguration; import glide.managers.CommandManager; import glide.managers.ConnectionManager; @@ -324,4 +329,30 @@ public CompletableFuture randomKey() { return commandManager.submitNewCommand( RandomKey, new String[0], this::handleStringOrNullResponse); } + + @Override + public CompletableFuture sort(@NonNull String key, @NonNull SortOptions sortOptions) { + String[] arguments = ArrayUtils.addFirst(sortOptions.toArgs(), key); + return commandManager.submitNewCommand( + Sort, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull String key, @NonNull SortOptions sortOptions) { + String[] arguments = ArrayUtils.addFirst(sortOptions.toArgs(), key); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortStore( + @NonNull String key, @NonNull String destination, @NonNull SortOptions sortOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + String[] arguments = + concatenateArrays(new String[] {key}, sortOptions.toArgs(), storeArguments); + return commandManager.submitNewCommand(Sort, arguments, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index b20f7b4202..0ac4374d87 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -2,6 +2,7 @@ package glide.api; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; @@ -33,6 +34,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RandomKey; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Time; import static redis_request.RedisRequestOuterClass.RequestType.UnWatch; @@ -45,6 +48,7 @@ import glide.api.models.ClusterValue; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -56,6 +60,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; import response.ResponseOuterClass.Response; /** @@ -701,4 +706,33 @@ public CompletableFuture randomKey() { return commandManager.submitNewCommand( RandomKey, new String[0], this::handleStringOrNullResponse); } + + @Override + public CompletableFuture sort( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + String[] arguments = ArrayUtils.addFirst(sortClusterOptions.toArgs(), key); + return commandManager.submitNewCommand( + Sort, arguments, response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortReadOnly( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + String[] arguments = ArrayUtils.addFirst(sortClusterOptions.toArgs(), key); + return commandManager.submitNewCommand( + SortReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture sortStore( + @NonNull String key, + @NonNull String destination, + @NonNull SortClusterOptions sortClusterOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + String[] arguments = + concatenateArrays(new String[] {key}, sortClusterOptions.toArgs(), storeArguments); + return commandManager.submitNewCommand(Sort, arguments, 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 c778d28f93..6c39ed8660 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -6,6 +6,7 @@ import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.RestoreOptions; import glide.api.models.commands.ScriptOptions; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** @@ -652,4 +653,63 @@ CompletableFuture pexpireAt( */ CompletableFuture restore( GlideString key, long ttl, byte[] value, RestoreOptions restoreOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String)}.
+ * + * @param key The key of the list, set, or sorted set to be sorted. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assertArrayEquals(new String[] {"1", "2", "3"}, client.sort("mylist").get()); // List is sorted in ascending order
+     * }
+ */ + CompletableFuture sort(String key); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assertArrayEquals(new String[] {"1", "2", "3"}, client.sortReadOnly("mylist").get()); // List is sorted in ascending order
+     * }
+ */ + CompletableFuture sortReadOnly(String key); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String)} or {@link + * #sortReadOnly(String)}. + * + * @apiNote When in cluster mode, key and destination must map to the + * same hash slot. + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2"}).get();
+     * assert client.sortStore("mylist", "destination").get() == 3;
+     * assertArrayEquals(
+     *    new String[] {"1", "2", "3"},
+     *    client.lrange("destination", 0, -1).get()); // Sorted list is stored in `destination`
+     * }
+ */ + CompletableFuture sortStore(String key, String destination); } diff --git a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java index a76290b3b3..74e287974f 100644 --- a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java @@ -4,6 +4,8 @@ import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; import glide.api.models.Transaction; +import glide.api.models.commands.SortClusterOptions; +import glide.api.models.configuration.ReadFrom; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import java.util.concurrent.CompletableFuture; @@ -148,4 +150,74 @@ public interface GenericClusterCommands { * } */ CompletableFuture randomKey(); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortClusterOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * String[] payload = client.sort("mylist", SortClusterOptions.builder().alpha()
+     *          .orderBy(DESC).limit(new SortBaseOptions.Limit(0L, 3L)).build()).get();
+     * assertArrayEquals(new String[] {"a", "3", "2"}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sort(String key, SortClusterOptions sortClusterOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * String[] payload = client.sortReadOnly("mylist", SortClusterOptions.builder().alpha()
+     *          .orderBy(DESC).limit(new SortBaseOptions.Limit(0L, 3L)).build()).get();
+     * assertArrayEquals(new String[] {"a", "3", "2"}, payload); // List is sorted in descending order lexicographically starting
+     * }
+ */ + CompletableFuture sortReadOnly(String key, SortClusterOptions sortClusterOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, + * SortClusterOptions)} or {@link #sortReadOnly(String, SortClusterOptions)}. + * + * @apiNote When in cluster mode, key and destination must map to the + * same hash slot. + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.lpush("mylist", new String[] {"3", "1", "2", "a"}).get();
+     * Long payload = client.sortStore("mylist", "destination",
+     *          SortClusterOptions.builder().alpha().orderBy(DESC)
+     *              .limit(new SortBaseOptions.Limit(0L, 3L))build()).get();
+     * assertEquals(3, payload);
+     * assertArrayEquals(
+     *      new String[] {"a", "3", "2"},
+     *      client.lrange("destination", 0, -1).get()); // Sorted list is stored in "destination"
+     * }
+ */ + CompletableFuture sortStore( + String key, String destination, SortClusterOptions sortClusterOptions); } 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 4823f08a09..44e53fb298 100644 --- a/java/client/src/main/java/glide/api/commands/GenericCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericCommands.java @@ -2,6 +2,8 @@ package glide.api.commands; import glide.api.models.Transaction; +import glide.api.models.commands.SortOptions; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** @@ -132,4 +134,74 @@ CompletableFuture copy( * } */ CompletableFuture randomKey(); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * String [] payload = client.sort("user_ids", SortOptions.builder().byPattern("user:*->age")
+     *                  .getPattern("user:*->name").build()).get();
+     * assertArrayEquals(new String[] {"Bob", "Alice"}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sort(String key, SortOptions sortOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. + * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return An Array of sorted elements. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * String [] payload = client.sortReadOnly("user_ids", SortOptions.builder().byPattern("user:*->age")
+     *                  .getPattern("user:*->name").build()).get();
+     * assertArrayEquals(new String[] {"Bob", "Alice"}, payload); // Returns a list of the names sorted by age
+     * }
+ */ + CompletableFuture sortReadOnly(String key, SortOptions sortOptions); + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @param destination The key where the sorted result will be stored. + * @return The number of elements in the sorted key stored at destination. + * @example + *
{@code
+     * client.hset("user:1", Map.of("name", "Alice", "age", "30")).get();
+     * client.hset("user:2", Map.of("name", "Bob", "age", "25")).get();
+     * client.lpush("user_ids", new String[] {"2", "1"}).get();
+     * Long payload = client.sortStore("user_ids", "destination",
+     *          SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build())
+     *          .get();
+     * assertEquals(2, payload);
+     * assertArrayEquals(
+     *      new String[] {"Bob", "Alice"},
+     *      client.lrange("destination", 0, -1).get()); // The list of the names sorted by age is stored in `destination`
+     * }
+ */ + CompletableFuture sortStore(String key, String destination, SortOptions sortOptions); } 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 ddf145c8d6..d7ccb07bbf 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -12,6 +12,7 @@ import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; import static glide.api.commands.StringBaseCommands.LEN_REDIS_API; import static glide.api.models.commands.RangeOptions.createZRangeArgs; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; @@ -143,6 +144,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Set; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -4752,6 +4755,57 @@ public T sunion(@NonNull String[] keys) { return getThis(); } + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String)}.
+ * + * @param key The key of the list, set, or sorted set to be sorted. + * @return Command Response - An Array of sorted elements. + */ + public T sort(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return getThis(); + } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements. + * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @return Command Response - An Array of sorted elements. + */ + public T sortReadOnly(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + protobufTransaction.addCommands(buildCommand(SortReadOnly, commandArgs)); + return getThis(); + } + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String)} or {@link + * #sortReadOnly(String)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public T sortStore(@NonNull String key, @NonNull String destination) { + ArgsArray commandArgs = buildArgs(new String[] {key, STORE_COMMAND_STRING, destination}); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return getThis(); + } + /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { return buildCommand(requestType, buildArgs()); diff --git a/java/client/src/main/java/glide/api/models/ClusterTransaction.java b/java/client/src/main/java/glide/api/models/ClusterTransaction.java index e2c4820057..43f614e04e 100644 --- a/java/client/src/main/java/glide/api/models/ClusterTransaction.java +++ b/java/client/src/main/java/glide/api/models/ClusterTransaction.java @@ -1,7 +1,16 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.utils.ArrayTransformUtils.concatenateArrays; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; + +import glide.api.models.commands.SortClusterOptions; import lombok.AllArgsConstructor; +import lombok.NonNull; +import org.apache.commons.lang3.ArrayUtils; +import redis_request.RedisRequestOuterClass; /** * Extends BaseTransaction class for cluster mode commands. Transactions allow the execution of a @@ -27,4 +36,68 @@ public class ClusterTransaction extends BaseTransaction { protected ClusterTransaction getThis() { return this; } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortClusterOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - An Array of sorted elements. + */ + public ClusterTransaction sort( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + RedisRequestOuterClass.Command.ArgsArray commandArgs = + buildArgs(ArrayUtils.addFirst(sortClusterOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + *
+ * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - An Array of sorted elements. + */ + public ClusterTransaction sortReadOnly( + @NonNull String key, @NonNull SortClusterOptions sortClusterOptions) { + RedisRequestOuterClass.Command.ArgsArray commandArgs = + buildArgs(ArrayUtils.addFirst(sortClusterOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(SortReadOnly, commandArgs)); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, + * SortClusterOptions)} or {@link #sortReadOnly(String, SortClusterOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param destination The key where the sorted result will be stored. + * @param sortClusterOptions The {@link SortClusterOptions}. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public ClusterTransaction sortStore( + @NonNull String key, + @NonNull String destination, + @NonNull SortClusterOptions sortClusterOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + RedisRequestOuterClass.Command.ArgsArray commandArgs = + buildArgs( + concatenateArrays(new String[] {key}, sortClusterOptions.toArgs(), storeArguments)); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return this; + } } 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 835fdc98e9..6ac58a33d7 100644 --- a/java/client/src/main/java/glide/api/models/Transaction.java +++ b/java/client/src/main/java/glide/api/models/Transaction.java @@ -3,10 +3,16 @@ import static glide.api.commands.GenericBaseCommands.REPLACE_REDIS_API; import static glide.api.commands.GenericCommands.DB_REDIS_API; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; +import static glide.utils.ArrayTransformUtils.concatenateArrays; import static redis_request.RedisRequestOuterClass.RequestType.Copy; import static redis_request.RedisRequestOuterClass.RequestType.Move; import static redis_request.RedisRequestOuterClass.RequestType.Select; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; +import glide.api.models.commands.SortOptions; import lombok.AllArgsConstructor; import lombok.NonNull; import org.apache.commons.lang3.ArrayUtils; @@ -111,4 +117,58 @@ public Transaction copy( protobufTransaction.addCommands(buildCommand(Copy, commandArgs)); return this; } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sort command can be used to sort elements based on different criteria and + * apply transformations on sorted elements.
+ * To store the result into a new key, see {@link #sortStore(String, String, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return Command Response - An Array of sorted elements. + */ + public Transaction sort(@NonNull String key, @NonNull SortOptions sortOptions) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sortOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(Sort, commandArgs)); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and returns the result. + * The sortReadOnly command can be used to sort elements based on different criteria + * and apply transformations on sorted elements.
+ * + * @since Redis 7.0 and above. + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @return Command Response - An Array of sorted elements. + */ + public Transaction sortReadOnly(@NonNull String key, @NonNull SortOptions sortOptions) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(sortOptions.toArgs(), key)); + protobufTransaction.addCommands(buildCommand(SortReadOnly, commandArgs)); + return this; + } + + /** + * Sorts the elements in the list, set, or sorted set at key and stores the result in + * destination. The sort command can be used to sort elements based on + * different criteria, apply transformations on sorted elements, and store the result in a new + * key.
+ * To get the sort result without storing it into a key, see {@link #sort(String, SortOptions)}. + * + * @param key The key of the list, set, or sorted set to be sorted. + * @param sortOptions The {@link SortOptions}. + * @param destination The key where the sorted result will be stored. + * @return Command Response - The number of elements in the sorted key stored at destination + * . + */ + public Transaction sortStore( + @NonNull String key, @NonNull String destination, @NonNull SortOptions sortOptions) { + String[] storeArguments = new String[] {STORE_COMMAND_STRING, destination}; + ArgsArray arguments = + buildArgs(concatenateArrays(new String[] {key}, sortOptions.toArgs(), storeArguments)); + protobufTransaction.addCommands(buildCommand(Sort, arguments)); + return this; + } } diff --git a/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java b/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java new file mode 100644 index 0000000000..3955acad71 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortBaseOptions.java @@ -0,0 +1,109 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to sort, sortReadOnly, and sortStore commands + * + * @see redis.io and redis.io + */ +@SuperBuilder +public abstract class SortBaseOptions { + /** + * LIMIT subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String LIMIT_COMMAND_STRING = "LIMIT"; + + /** + * ALPHA subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String ALPHA_COMMAND_STRING = "ALPHA"; + + /** STORE subcommand string to include in the SORT command. */ + public static final String STORE_COMMAND_STRING = "STORE"; + + /** + * Limiting the range of the query by setting offset and result count. See {@link Limit} class for + * more information. + */ + private final Limit limit; + + /** Options for sorting order of elements. */ + private final OrderBy orderBy; + + /** + * When true, sorts elements lexicographically. When false (default), + * sorts elements numerically. Use this when the list, set, or sorted set contains string values + * that cannot be converted into double precision floating point numbers. + */ + private final boolean isAlpha; + + public abstract static class SortBaseOptionsBuilder< + C extends SortBaseOptions, B extends SortBaseOptionsBuilder> { + public B alpha() { + this.isAlpha = true; + return self(); + } + } + + /** + * The LIMIT argument is commonly used to specify a subset of results from the + * matching elements, similar to the LIMIT clause in SQL (e.g., `SELECT LIMIT offset, + * count`). + */ + @RequiredArgsConstructor + public static final class Limit { + /** The starting position of the range, zero based. */ + private final long offset; + + /** + * The maximum number of elements to include in the range. A negative count returns all elements + * from the offset. + */ + private final long count; + } + + /** + * Specifies the order to sort the elements. Can be ASC (ascending) or DESC + * (descending). + */ + @RequiredArgsConstructor + public enum OrderBy { + ASC, + DESC + } + + /** + * Creates the arguments to be used in SORT and SORT_RO commands. + * + * @return a String array that holds the sub commands and their arguments. + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(); + + if (limit != null) { + optionArgs.addAll( + List.of( + LIMIT_COMMAND_STRING, + Long.toString(this.limit.offset), + Long.toString(this.limit.count))); + } + + if (orderBy != null) { + optionArgs.add(this.orderBy.toString()); + } + + if (isAlpha) { + optionArgs.add(ALPHA_COMMAND_STRING); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java b/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java new file mode 100644 index 0000000000..8d8a2a77e4 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortClusterOptions.java @@ -0,0 +1,13 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericBaseCommands; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericBaseCommands#sort(String, SortClusterOptions)}, {@link + * GenericBaseCommands#sortReadOnly(String, SortClusterOptions)}, and {@link + * GenericBaseCommands#sortStore(String, String, SortClusterOptions)} + */ +@SuperBuilder +public class SortClusterOptions extends SortBaseOptions {} diff --git a/java/client/src/main/java/glide/api/models/commands/SortOptions.java b/java/client/src/main/java/glide/api/models/commands/SortOptions.java new file mode 100644 index 0000000000..1044bc03ba --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/SortOptions.java @@ -0,0 +1,77 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericCommands; +import java.util.ArrayList; +import java.util.List; +import lombok.Singular; +import lombok.experimental.SuperBuilder; + +/** + * Optional arguments to {@link GenericCommands#sort(String, SortOptions)}, {@link + * GenericCommands#sortReadOnly(String, SortOptions)}, and {@link GenericCommands#sortStore(String, + * String, SortOptions)} + * + * @see redis.io and redis.io + */ +@SuperBuilder +public class SortOptions extends SortBaseOptions { + /** + * BY subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String BY_COMMAND_STRING = "BY"; + + /** + * GET subcommand string to include in the SORT and SORT_RO + * commands. + */ + public static final String GET_COMMAND_STRING = "GET"; + + /** + * A pattern to sort by external keys instead of by the elements stored at the key themselves. The + * pattern should contain an asterisk (*) as a placeholder for the element values, where the value + * from the key replaces the asterisk to create the key name. For example, if key + * contains IDs of objects, byPattern can be used to sort these IDs based on an + * attribute of the objects, like their weights or timestamps. + */ + private final String byPattern; + + /** + * A pattern used to retrieve external keys' values, instead of the elements at key. + * The pattern should contain an asterisk (*) as a placeholder for the element values, where the + * value from key replaces the asterisk to create the key name. This + * allows the sorted elements to be transformed based on the related keys values. For example, if + * key contains IDs of users, getPatterns can be used to retrieve + * specific attributes of these users, such as their names or email addresses. E.g., if + * getPatterns is name_*, the command will return the values of the keys + * name_<element> for each sorted element. Multiple getPatterns + * arguments can be provided to retrieve multiple attributes. The special value # can + * be used to include the actual element from key being sorted. If not provided, only + * the sorted elements themselves are returned.
+ * + * @see valkey.io for more information. + */ + @Singular private final List getPatterns; + + /** + * Creates the arguments to be used in SORT and SORT_RO commands. + * + * @return a String array that holds the sub commands and their arguments. + */ + public String[] toArgs() { + List optionArgs = new ArrayList<>(List.of(super.toArgs())); + + if (byPattern != null) { + optionArgs.addAll(List.of(BY_COMMAND_STRING, byPattern)); + } + + if (getPatterns != null) { + getPatterns.stream() + .forEach(getPattern -> optionArgs.addAll(List.of(GET_COMMAND_STRING, getPattern))); + } + + return optionArgs.toArray(new String[0]); + } +} diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 6cecdd1da1..d8e4014504 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -20,6 +20,11 @@ import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.BY_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow.BitOverflowControl.SAT; import static glide.api.models.commands.bitmap.BitFieldOptions.GET_COMMAND_STRING; import static glide.api.models.commands.bitmap.BitFieldOptions.INCRBY_COMMAND_STRING; @@ -184,6 +189,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Select; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -255,6 +262,8 @@ import glide.api.models.commands.ScriptOptions; import glide.api.models.commands.SetOptions; import glide.api.models.commands.SetOptions.Expiry; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.commands.WeightAggregateOptions.Aggregate; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; @@ -6801,4 +6810,131 @@ public void restore_with_restoreOptions_returns_success() { assertEquals(testResponse, response); assertEquals(OK, response.get()); } + + @SneakyThrows + @Test + public void sort_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + byPattern, + GET_COMMAND_STRING, + getPattern + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortOptions.builder() + .alpha() + .limit(new SortBaseOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPattern(getPattern) + .byPattern(byPattern) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] {key, BY_COMMAND_STRING, byPattern, GET_COMMAND_STRING, getPattern}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, SortOptions.builder().getPattern(getPattern).byPattern(byPattern).build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_with_options_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + Long limitOffset = 0L; + Long limitCount = 2L; + String byPattern = "byPattern"; + String getPattern = "getPattern"; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + byPattern, + GET_COMMAND_STRING, + getPattern, + STORE_COMMAND_STRING, + destKey + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortOptions.builder() + .alpha() + .limit(new SortBaseOptions.Limit(limitOffset, limitCount)) + .orderBy(DESC) + .getPattern(getPattern) + .byPattern(byPattern) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } } diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index be31009fae..26a3cef828 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -5,6 +5,10 @@ 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.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; @@ -41,6 +45,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Lolwut; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RandomKey; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Time; import static redis_request.RedisRequestOuterClass.RequestType.UnWatch; @@ -48,6 +54,8 @@ import glide.api.models.ClusterValue; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortBaseOptions.Limit; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.commands.function.FunctionLoadOptions; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -1898,4 +1906,195 @@ public void randomKey() { // verify assertEquals(testResponse, response); } + + @SneakyThrows + @Test + public void sort_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sort(key); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sort_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sort( + key, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sortReadOnly(key); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortReadOnly_with_options_returns_success() { + // setup + String[] result = new String[] {"1", "2", "3"}; + String key = "key"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SortReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortReadOnly( + key, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(Sort), eq(new String[] {key, STORE_COMMAND_STRING, destKey}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.sortStore(key, destKey); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void sortStore_with_options_returns_success() { + // setup + Long result = 5L; + String key = "key"; + String destKey = "destKey"; + Long limitOffset = 0L; + Long limitCount = 2L; + String[] args = + new String[] { + key, + LIMIT_COMMAND_STRING, + limitOffset.toString(), + limitCount.toString(), + DESC.toString(), + ALPHA_COMMAND_STRING, + STORE_COMMAND_STRING, + destKey + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Sort), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.sortStore( + key, + destKey, + SortClusterOptions.builder() + .alpha() + .limit(new Limit(limitOffset, limitCount)) + .orderBy(DESC) + .build()); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } } diff --git a/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java new file mode 100644 index 0000000000..389c66f654 --- /dev/null +++ b/java/client/src/test/java/glide/api/models/ClusterTransactionTests.java @@ -0,0 +1,94 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models; + +import static glide.api.models.TransactionTests.buildArgs; +import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.OrderBy.ASC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; + +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortClusterOptions; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import redis_request.RedisRequestOuterClass; + +public class ClusterTransactionTests { + private static Stream getTransactionBuilders() { + return Stream.of( + Arguments.of(new ClusterTransaction()), Arguments.of(new ClusterTransaction())); + } + + @ParameterizedTest + @MethodSource("getTransactionBuilders") + public void cluster_transaction_builds_protobuf_request(ClusterTransaction transaction) { + List> + results = new LinkedList<>(); + + transaction.sortReadOnly( + "key1", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", LIMIT_COMMAND_STRING, "0", "1", ASC.toString(), ALPHA_COMMAND_STRING))); + + transaction.sort( + "key1", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", LIMIT_COMMAND_STRING, "0", "1", ASC.toString(), ALPHA_COMMAND_STRING))); + + transaction.sortStore( + "key1", + "key2", + SortClusterOptions.builder() + .orderBy(ASC) + .alpha() + .limit(new SortBaseOptions.Limit(0L, 1L)) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + ASC.toString(), + ALPHA_COMMAND_STRING, + STORE_COMMAND_STRING, + "key2"))); + + var protobufTransaction = transaction.getProtobufTransaction().build(); + + for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { + RedisRequestOuterClass.Command protobuf = protobufTransaction.getCommands(idx); + + assertEquals(results.get(idx).getLeft(), protobuf.getRequestType()); + assertEquals( + results.get(idx).getRight().getArgsCount(), protobuf.getArgsArray().getArgsCount()); + assertEquals(results.get(idx).getRight(), protobuf.getArgsArray()); + } + } +} 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 a3f47e2e61..15bf21a04d 100644 --- a/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java +++ b/java/client/src/test/java/glide/api/models/StandaloneTransactionTests.java @@ -4,11 +4,21 @@ 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 glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.LIMIT_COMMAND_STRING; +import static glide.api.models.commands.SortBaseOptions.Limit; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.BY_COMMAND_STRING; +import static glide.api.models.commands.SortOptions.GET_COMMAND_STRING; 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; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; +import glide.api.models.commands.SortOptions; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.tuple.Pair; @@ -29,6 +39,139 @@ public void standalone_transaction_commands() { transaction.copy("key1", "key2", 1, true); results.add(Pair.of(Copy, buildArgs("key1", "key2", DB_REDIS_API, "1", REPLACE_REDIS_API))); + transaction.sort( + "key1", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sort( + "key1", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortReadOnly( + "key1", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortReadOnly( + "key1", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + SortReadOnly, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2"))); + transaction.sortStore( + "key1", + "key2", + SortOptions.builder() + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2", + STORE_COMMAND_STRING, + "key2"))); + transaction.sortStore( + "key1", + "key2", + SortOptions.builder() + .orderBy(DESC) + .alpha() + .limit(new Limit(0L, 1L)) + .byPattern("byPattern") + .getPatterns(List.of("getPattern1", "getPattern2")) + .build()); + results.add( + Pair.of( + Sort, + buildArgs( + "key1", + LIMIT_COMMAND_STRING, + "0", + "1", + DESC.toString(), + ALPHA_COMMAND_STRING, + BY_COMMAND_STRING, + "byPattern", + GET_COMMAND_STRING, + "getPattern1", + GET_COMMAND_STRING, + "getPattern2", + STORE_COMMAND_STRING, + "key2"))); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { 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 92ec6102a6..7b4218e47d 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -18,6 +18,7 @@ import static glide.api.models.commands.ScoreFilter.MAX; import static glide.api.models.commands.ScoreFilter.MIN; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.WeightAggregateOptions.AGGREGATE_REDIS_API; import static glide.api.models.commands.WeightAggregateOptions.WEIGHTS_REDIS_API; import static glide.api.models.commands.ZAddOptions.UpdateOptions.SCORE_LESS_THAN_CURRENT; @@ -159,6 +160,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.Set; import static redis_request.RedisRequestOuterClass.RequestType.SetBit; import static redis_request.RedisRequestOuterClass.RequestType.SetRange; +import static redis_request.RedisRequestOuterClass.RequestType.Sort; +import static redis_request.RedisRequestOuterClass.RequestType.SortReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.Strlen; import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Time; @@ -1097,6 +1100,13 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.sunion(new String[] {"key1", "key2"}); results.add(Pair.of(SUnion, buildArgs("key1", "key2"))); + transaction.sort("key1"); + results.add(Pair.of(Sort, buildArgs("key1"))); + transaction.sortReadOnly("key1"); + results.add(Pair.of(SortReadOnly, buildArgs("key1"))); + transaction.sortStore("key1", "key2"); + results.add(Pair.of(Sort, buildArgs("key1", STORE_COMMAND_STRING, "key2"))); + 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 5842f63ddf..5f5d97ec73 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -5890,4 +5890,37 @@ public void test_dump_restore_withOptions(BaseClient client) { .get()); assertInstanceOf(RequestException.class, executionException.getCause()); } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void sort(BaseClient client) { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String[] key1LpushArgs = {"2", "1", "4", "3"}; + String[] key1AscendingList = {"1", "2", "3", "4"}; + String[] key2LpushArgs = {"2", "1", "a", "x", "c", "4", "3"}; + + assertArrayEquals(new String[0], client.sort(key3).get()); + assertEquals(4, client.lpush(key1, key1LpushArgs).get()); + assertArrayEquals(key1AscendingList, client.sort(key1).get()); + + // SORT_R0 + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals(new String[0], client.sortReadOnly(key3).get()); + assertArrayEquals(key1AscendingList, client.sortReadOnly(key1).get()); + } + + // SORT with STORE + assertEquals(4, client.sortStore(key1, key3).get()); + assertArrayEquals(key1AscendingList, client.lrange(key3, 0, -1).get()); + + // Exceptions + // SORT with strings require ALPHA + assertEquals(7, client.lpush(key2, key2LpushArgs).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.sort(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 6d44d664dd..37fe237d5f 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -106,6 +106,8 @@ private static Object[] genericCommands(BaseTransaction transaction) { String genericKey2 = "{GenericKey}-2-" + UUID.randomUUID(); String genericKey3 = "{GenericKey}-3-" + UUID.randomUUID(); String genericKey4 = "{GenericKey}-4-" + UUID.randomUUID(); + String[] ascendingList = new String[] {"1", "2", "3"}; + String[] descendingList = new String[] {"3", "2", "1"}; transaction .set(genericKey1, value1) @@ -127,7 +129,11 @@ private static Object[] genericCommands(BaseTransaction transaction) { .expireAt(genericKey1, 42) // expire (delete) key immediately .pexpire(genericKey1, 42) .pexpireAt(genericKey1, 42) - .ttl(genericKey2); + .ttl(genericKey2) + .lpush(genericKey3, new String[] {"3", "1", "2"}) + .sort(genericKey3) + .sortStore(genericKey3, genericKey4) + .lrange(genericKey4, 0, -1); if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { transaction @@ -137,7 +143,8 @@ private static Object[] genericCommands(BaseTransaction transaction) { .pexpire(genericKey1, 42, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT) .pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) .expiretime(genericKey1) - .pexpiretime(genericKey1); + .pexpiretime(genericKey1) + .sortReadOnly(genericKey3); } if (REDIS_VERSION.isGreaterThanOrEqualTo("6.2.0")) { @@ -170,6 +177,10 @@ private static Object[] genericCommands(BaseTransaction transaction) { false, // pexpire(genericKey1, 42) false, // pexpireAt(genericKey1, 42) -2L, // ttl(genericKey2) + 3L, // lpush(genericKey3, new String[] {"3", "1", "2"}) + ascendingList, // sort(genericKey3) + 3L, // sortStore(genericKey3, genericKey4) + ascendingList, // lrange(genericKey4, 0, -1) }; if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { @@ -184,6 +195,7 @@ private static Object[] genericCommands(BaseTransaction transaction) { false, // pexpireAt(genericKey1, 42, ExpireOptions.HAS_NO_EXPIRY) -2L, // expiretime(genericKey1) -2L, // pexpiretime(genericKey1) + ascendingList, // sortReadOnly(genericKey3) }); } diff --git a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java index d1f1eb4452..2bd943a15a 100644 --- a/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java +++ b/java/integTest/src/test/java/glide/cluster/ClusterTransactionTests.java @@ -4,8 +4,10 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestUtilities.assertDeepEquals; import static glide.api.BaseClient.OK; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; +import static glide.utils.ArrayTransformUtils.concatenateArrays; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -15,6 +17,7 @@ import glide.TransactionTestUtilities.TransactionBuilder; import glide.api.RedisClusterClient; import glide.api.models.ClusterTransaction; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -247,4 +250,40 @@ public void unwatch() { assertEquals(foobarString, clusterClient.get(key1).get()); assertEquals(foobarString, clusterClient.get(key2).get()); } + + @Test + @SneakyThrows + public void sort() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String[] descendingList = new String[] {"3", "2", "1"}; + ClusterTransaction transaction = new ClusterTransaction(); + transaction + .lpush(key1, new String[] {"3", "1", "2"}) + .sort(key1, SortClusterOptions.builder().orderBy(DESC).build()) + .sortStore(key1, key2, SortClusterOptions.builder().orderBy(DESC).build()) + .lrange(key2, 0, -1); + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction.sortReadOnly(key1, SortClusterOptions.builder().orderBy(DESC).build()); + } + + Object[] results = clusterClient.exec(transaction).get(); + Object[] expectedResult = + new Object[] { + 3L, // lpush(key1, new String[] {"3", "1", "2"}) + descendingList, // sort(key1, SortClusterOptions.builder().orderBy(DESC).build()) + 3L, // sortStore(key1, key2, DESC)) + descendingList, // lrange(key2, 0, -1) + }; + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResult = + concatenateArrays( + expectedResult, new Object[] {descendingList} // sortReadOnly(key1, DESC) + ); + } + + assertDeepEquals(expectedResult, results); + } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index dfab0682f7..8922482409 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -24,6 +24,7 @@ import static glide.api.models.commands.InfoOptions.Section.SERVER; import static glide.api.models.commands.InfoOptions.Section.STATS; import static glide.api.models.commands.ScoreFilter.MAX; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; import static glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; @@ -46,6 +47,8 @@ import glide.api.models.commands.InfoOptions; import glide.api.models.commands.ListDirection; import glide.api.models.commands.RangeOptions.RangeByIndex; +import glide.api.models.commands.SortBaseOptions; +import glide.api.models.commands.SortClusterOptions; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.configuration.RequestRoutingConfiguration.Route; @@ -792,7 +795,12 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { Arguments.of("msetnx", null, clusterClient.msetnx(Map.of("abc", "def", "ghi", "jkl"))), Arguments.of("lcs", "7.0.0", clusterClient.lcs("abc", "def")), Arguments.of("lcsLEN", "7.0.0", clusterClient.lcsLen("abc", "def")), - Arguments.of("sunion", "1.0.0", clusterClient.sunion(new String[] {"abc", "def", "ghi"}))); + Arguments.of("sunion", "1.0.0", clusterClient.sunion(new String[] {"abc", "def", "ghi"})), + Arguments.of("sortStore", "1.0.0", clusterClient.sortStore("abc", "def")), + Arguments.of( + "sortStore", + "1.0.0", + clusterClient.sortStore("abc", "def", SortClusterOptions.builder().alpha().build()))); } @SneakyThrows @@ -1607,4 +1615,90 @@ public void randomKey() { // uncomment when this is completed: https://github.com/amazon-contributing/redis-rs/pull/153 // assertNull(clusterClient.randomKey().get()); } + + @Test + @SneakyThrows + public void sort() { + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String[] key1LpushArgs = {"2", "1", "4", "3"}; + String[] key1AscendingList = {"1", "2", "3", "4"}; + String[] key1DescendingList = {"4", "3", "2", "1"}; + String[] key2LpushArgs = {"2", "1", "a", "x", "c", "4", "3"}; + String[] key2DescendingList = {"x", "c", "a", "4", "3", "2", "1"}; + String[] key2DescendingListSubset = Arrays.copyOfRange(key2DescendingList, 0, 4); + + assertArrayEquals(new String[0], clusterClient.sort(key3).get()); + assertEquals(4, clusterClient.lpush(key1, key1LpushArgs).get()); + assertArrayEquals( + new String[0], + clusterClient + .sort( + key1, SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 0L)).build()) + .get()); + assertArrayEquals( + key1DescendingList, + clusterClient.sort(key1, SortClusterOptions.builder().orderBy(DESC).build()).get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + clusterClient + .sort( + key1, SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 2L)).build()) + .get()); + assertEquals(7, clusterClient.lpush(key2, key2LpushArgs).get()); + assertArrayEquals( + key2DescendingListSubset, + clusterClient + .sort( + key2, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + + // SORT_R0 + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals( + key1DescendingList, + clusterClient + .sortReadOnly(key1, SortClusterOptions.builder().orderBy(DESC).build()) + .get()); + assertArrayEquals( + Arrays.copyOfRange(key1AscendingList, 0, 2), + clusterClient + .sortReadOnly( + key1, + SortClusterOptions.builder().limit(new SortBaseOptions.Limit(0L, 2L)).build()) + .get()); + assertArrayEquals( + key2DescendingListSubset, + clusterClient + .sortReadOnly( + key2, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + } + + // SORT with STORE + assertEquals( + 4, + clusterClient + .sortStore( + key2, + key3, + SortClusterOptions.builder() + .alpha() + .orderBy(DESC) + .limit(new SortBaseOptions.Limit(0L, 4L)) + .build()) + .get()); + assertArrayEquals(key2DescendingListSubset, clusterClient.lrange(key3, 0, -1).get()); + } } diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 60a9d00599..d18642e058 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -18,9 +18,13 @@ import static glide.api.models.commands.InfoOptions.Section.MEMORY; import static glide.api.models.commands.InfoOptions.Section.SERVER; import static glide.api.models.commands.InfoOptions.Section.STATS; +import static glide.api.models.commands.SortBaseOptions.Limit; +import static glide.api.models.commands.SortBaseOptions.OrderBy.ASC; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; import static glide.cluster.CommandTests.DEFAULT_INFO_SECTIONS; import static glide.cluster.CommandTests.EVERYTHING_INFO_SECTIONS; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -31,10 +35,12 @@ import glide.api.RedisClient; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -737,4 +743,178 @@ public void randomkey() { assertEquals(OK, regularClient.flushall().get()); assertNull(regularClient.randomKey().get()); } + + @Test + @SneakyThrows + public void sort() { + String setKey1 = "setKey1"; + String setKey2 = "setKey2"; + String setKey3 = "setKey3"; + String setKey4 = "setKey4"; + String setKey5 = "setKey5"; + String[] setKeys = new String[] {setKey1, setKey2, setKey3, setKey4, setKey5}; + String listKey = "listKey"; + String storeKey = "storeKey"; + String nameField = "name"; + String ageField = "age"; + String[] names = new String[] {"Alice", "Bob", "Charlie", "Dave", "Eve"}; + String[] namesSortedByAge = new String[] {"Dave", "Bob", "Alice", "Charlie", "Eve"}; + String[] ages = new String[] {"30", "25", "35", "20", "40"}; + String[] userIDs = new String[] {"3", "1", "5", "4", "2"}; + String namePattern = "setKey*->name"; + String agePattern = "setKey*->age"; + String missingListKey = "100000"; + + for (int i = 0; i < setKeys.length; i++) { + assertEquals( + 2, regularClient.hset(setKeys[i], Map.of(nameField, names[i], ageField, ages[i])).get()); + } + + assertEquals(5, regularClient.rpush(listKey, userIDs).get()); + assertArrayEquals( + new String[] {"Alice", "Bob"}, + regularClient + .sort( + listKey, + SortOptions.builder().limit(new Limit(0L, 2L)).getPattern(namePattern).build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "Dave"}, + regularClient + .sort( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "40", "Charlie", "35"}, + regularClient + .sort( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(List.of(namePattern, agePattern)) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient.sort(listKey, SortOptions.builder().byPattern("noSort").build()).get()); + + // Non-existent key in the GET pattern results in nulls + assertArrayEquals( + new String[] {null, null, null, null, null}, + regularClient + .sort(listKey, SortOptions.builder().alpha().getPattern("missing").build()) + .get()); + + // Missing key in the set + assertEquals(6, regularClient.lpush(listKey, new String[] {missingListKey}).get()); + assertArrayEquals( + new String[] {null, "Dave", "Bob", "Alice", "Charlie", "Eve"}, + regularClient + .sort( + listKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertEquals(missingListKey, regularClient.lpop(listKey).get()); + + // SORT_RO + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + assertArrayEquals( + new String[] {"Alice", "Bob"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder().limit(new Limit(0L, 2L)).getPattern(namePattern).build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "Dave"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals( + new String[] {"Eve", "40", "Charlie", "35"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder() + .limit(new Limit(0L, 2L)) + .orderBy(DESC) + .byPattern(agePattern) + .getPatterns(List.of(namePattern, agePattern)) + .build()) + .get()); + + // Non-existent key in the BY pattern will result in skipping the sorting operation + assertArrayEquals( + userIDs, + regularClient + .sortReadOnly(listKey, SortOptions.builder().byPattern("noSort").build()) + .get()); + + // Non-existent key in the GET pattern results in nulls + assertArrayEquals( + new String[] {null, null, null, null, null}, + regularClient + .sortReadOnly(listKey, SortOptions.builder().alpha().getPattern("missing").build()) + .get()); + + assertArrayEquals( + namesSortedByAge, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + + // Missing key in the set + assertEquals(6, regularClient.lpush(listKey, new String[] {missingListKey}).get()); + assertArrayEquals( + new String[] {null, "Dave", "Bob", "Alice", "Charlie", "Eve"}, + regularClient + .sortReadOnly( + listKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertEquals(missingListKey, regularClient.lpop(listKey).get()); + } + + // SORT with STORE + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortOptions.builder() + .limit(new Limit(0L, -1L)) + .orderBy(ASC) + .byPattern(agePattern) + .getPattern(namePattern) + .build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey, 0, -1).get()); + assertEquals( + 5, + regularClient + .sortStore( + listKey, + storeKey, + SortOptions.builder().byPattern(agePattern).getPattern(namePattern).build()) + .get()); + assertArrayEquals(namesSortedByAge, regularClient.lrange(storeKey, 0, -1).get()); + } } diff --git a/java/integTest/src/test/java/glide/standalone/TransactionTests.java b/java/integTest/src/test/java/glide/standalone/TransactionTests.java index e3d532d325..0543410ba2 100644 --- a/java/integTest/src/test/java/glide/standalone/TransactionTests.java +++ b/java/integTest/src/test/java/glide/standalone/TransactionTests.java @@ -5,6 +5,7 @@ import static glide.TestUtilities.assertDeepEquals; import static glide.TestUtilities.commonClientConfig; import static glide.api.BaseClient.OK; +import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -17,6 +18,7 @@ import glide.api.RedisClient; import glide.api.models.Transaction; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.SortOptions; import glide.api.models.exceptions.RequestException; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -328,4 +330,81 @@ public void unwatch() { assertEquals(foobarString, client.get(key1).get()); assertEquals(foobarString, client.get(key2).get()); } + + @Test + @SneakyThrows + public void sort_and_sortReadOnly() { + Transaction transaction1 = new Transaction(); + Transaction transaction2 = new Transaction(); + String genericKey1 = "{GenericKey}-1-" + UUID.randomUUID(); + String genericKey2 = "{GenericKey}-2-" + UUID.randomUUID(); + String[] ascendingListByAge = new String[] {"Bob", "Alice"}; + String[] descendingListByAge = new String[] {"Alice", "Bob"}; + + transaction1 + .hset("user:1", Map.of("name", "Alice", "age", "30")) + .hset("user:2", Map.of("name", "Bob", "age", "25")) + .lpush(genericKey1, new String[] {"2", "1"}) + .sort( + genericKey1, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .sort( + genericKey1, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()) + .sortStore( + genericKey1, + genericKey2, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .lrange(genericKey2, 0, -1) + .sortStore( + genericKey1, + genericKey2, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()) + .lrange(genericKey2, 0, -1); + + var expectedResults = + new Object[] { + 2L, // hset("user:1", Map.of("name", "Alice", "age", "30")) + 2L, // hset("user:2", Map.of("name", "Bob", "age", "25")) + 2L, // lpush(genericKey1, new String[] {"2", "1"}) + ascendingListByAge, // sort(genericKey1, SortOptions) + descendingListByAge, // sort(genericKey1, SortOptions) + 2L, // sortStore(genericKey1, genericKey2, SortOptions) + ascendingListByAge, // lrange(genericKey4, 0, -1) + 2L, // sortStore(genericKey1, genericKey2, SortOptions) + descendingListByAge, // lrange(genericKey2, 0, -1) + }; + + assertArrayEquals(expectedResults, client.exec(transaction1).get()); + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction2 + .sortReadOnly( + genericKey1, + SortOptions.builder().byPattern("user:*->age").getPattern("user:*->name").build()) + .sortReadOnly( + genericKey1, + SortOptions.builder() + .orderBy(DESC) + .byPattern("user:*->age") + .getPattern("user:*->name") + .build()); + + expectedResults = + new Object[] { + ascendingListByAge, // sortReadOnly(genericKey1, SortOptions) + descendingListByAge, // sortReadOnly(genericKey1, SortOptions) + }; + + assertArrayEquals(expectedResults, client.exec(transaction2).get()); + } + } } From c921f136886d6a764896e96a14cc11ec0ab98f23 Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Fri, 21 Jun 2024 21:47:01 -0700 Subject: [PATCH 03/17] Python: add XREVRANGE command (#1625) * Python: add XREVRANGE command * Update doc for xrevrange Signed-off-by: Andrew Carbonetto * Update transaction docs Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Andrew Carbonetto Co-authored-by: Andrew Carbonetto --- CHANGELOG.md | 1 + python/python/glide/async_commands/core.py | 52 ++++++++++++++++++- .../glide/async_commands/transaction.py | 40 +++++++++++++- python/python/tests/test_async_client.py | 24 ++++++++- python/python/tests/test_transaction.py | 2 + 5 files changed, 115 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 807b09c548..d1d018d210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ * 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)) +* Python: Added XREVRANGE command ([#1625](https://github.com/aws/glide-for-redis/pull/1625)) ### 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/async_commands/core.py b/python/python/glide/async_commands/core.py index d38b8a185d..c131127d6e 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -2683,7 +2683,8 @@ async def xrange( Returns: Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a - list of pairings with format `[[field, entry], [field, entry], ...]`. + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns null if the range + arguments are not applicable. Examples: >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="0-1")) @@ -2703,6 +2704,55 @@ async def xrange( await self._execute_command(RequestType.XRange, args), ) + async def xrevrange( + self, + key: str, + end: StreamRangeBound, + start: StreamRangeBound, + count: Optional[int] = None, + ) -> Optional[Mapping[str, List[List[str]]]]: + """ + Returns stream entries matching a given range of IDs in reverse order. Equivalent to `XRANGE` but returns the + entries in reverse order. + + See https://valkey.io/commands/xrevrange for more details. + + Args: + key (str): The key of the stream. + end (StreamRangeBound): The ending stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MaxId` to end with the maximum available ID. + start (StreamRangeBound): The starting stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MinId` to start with the minimum available ID. + count (Optional[int]): An optional argument specifying the maximum count of stream entries to return. + If `count` is not provided, all stream entries in the range will be returned. + + Returns: + Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns null if the range + arguments are not applicable. + + Examples: + >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="0-1")) + >>> await client.xadd("mystream", [("field2", "value2"), ("field2", "value3")], StreamAddOptions(id="0-2")) + >>> await client.xrevrange("mystream", MaxId(), MinId()) + { + "0-2": [["field2", "value2"], ["field2", "value3"]], + "0-1": [["field1", "value1"]], + } # Indicates the stream IDs and their associated field-value pairs for all stream entries in "mystream". + """ + args = [key, end.to_arg(), start.to_arg()] + if count is not None: + args.extend(["COUNT", str(count)]) + + return cast( + Optional[Mapping[str, List[List[str]]]], + await self._execute_command(RequestType.XRevRange, args), + ) + async def geoadd( self, key: str, diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index adced407f8..ba8e685f37 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -1879,7 +1879,8 @@ def xrange( Command response: Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a - list of pairings with format `[[field, entry], [field, entry], ...]`. + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns null if the range arguments + are not applicable. """ args = [key, start.to_arg(), end.to_arg()] if count is not None: @@ -1887,6 +1888,43 @@ def xrange( return self.append_command(RequestType.XRange, args) + def xrevrange( + self: TTransaction, + key: str, + end: StreamRangeBound, + start: StreamRangeBound, + count: Optional[int] = None, + ) -> TTransaction: + """ + Returns stream entries matching a given range of IDs in reverse order. Equivalent to `XRANGE` but returns the + entries in reverse order. + + See https://valkey.io/commands/xrevrange for more details. + + Args: + key (str): The key of the stream. + end (StreamRangeBound): The ending stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MaxId` to end with the maximum available ID. + start (StreamRangeBound): The starting stream ID bound for the range. + - Use `IdBound` to specify a stream ID. + - Use `ExclusiveIdBound` to specify an exclusive bounded stream ID. + - Use `MinId` to start with the minimum available ID. + count (Optional[int]): An optional argument specifying the maximum count of stream entries to return. + If `count` is not provided, all stream entries in the range will be returned. + + Command response: + Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns null if the range arguments + are not applicable. + """ + args = [key, end.to_arg(), start.to_arg()] + if count is not None: + args.extend(["COUNT", str(count)]) + + return self.append_command(RequestType.XRevRange, args) + def geoadd( self: TTransaction, key: str, diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index cdaa33f8b7..20be6a28f6 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -4811,7 +4811,7 @@ async def test_xdel(self, redis_client: TRedisClient): @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_xrange(self, redis_client: TRedisClient): + async def test_xrange_and_xrevrange(self, redis_client: TRedisClient): key = get_random_string(10) non_existing_key = get_random_string(10) string_key = get_random_string(10) @@ -4838,9 +4838,15 @@ async def test_xrange(self, redis_client: TRedisClient): stream_id1: [["f1", "v1"]], stream_id2: [["f2", "v2"]], } + assert await redis_client.xrevrange(key, MaxId(), MinId()) == { + stream_id2: [["f2", "v2"]], + stream_id1: [["f1", "v1"]], + } # returns empty mapping if + before - assert await redis_client.xrange(key, MaxId(), MinId()) == {} + # rev search returns empty mapping if - before + + assert await redis_client.xrevrange(key, MinId(), MaxId()) == {} assert ( await redis_client.xadd( @@ -4848,33 +4854,47 @@ async def test_xrange(self, redis_client: TRedisClient): ) == stream_id3 ) + # get the newest entry assert await redis_client.xrange( key, ExclusiveIdBound(stream_id2), ExclusiveIdBound.from_timestamp(5), 1 ) == {stream_id3: [["f3", "v3"]]} + assert await redis_client.xrevrange( + key, ExclusiveIdBound.from_timestamp(5), ExclusiveIdBound(stream_id2), 1 + ) == {stream_id3: [["f3", "v3"]]} - # xrange against an emptied stream + # xrange/xrevrange against an emptied stream assert await redis_client.xdel(key, [stream_id1, stream_id2, stream_id3]) == 3 assert await redis_client.xrange(key, MinId(), MaxId(), 10) == {} + assert await redis_client.xrevrange(key, MaxId(), MinId(), 10) == {} assert await redis_client.xrange(non_existing_key, MinId(), MaxId()) == {} + assert await redis_client.xrevrange(non_existing_key, MaxId(), MinId()) == {} # count value < 1 returns None assert await redis_client.xrange(key, MinId(), MaxId(), 0) is None assert await redis_client.xrange(key, MinId(), MaxId(), -1) is None + assert await redis_client.xrevrange(key, MaxId(), MinId(), 0) is None + assert await redis_client.xrevrange(key, MaxId(), MinId(), -1) is None # key exists, but it is not a stream assert await redis_client.set(string_key, "foo") with pytest.raises(RequestError): await redis_client.xrange(string_key, MinId(), MaxId()) + with pytest.raises(RequestError): + await redis_client.xrevrange(string_key, MaxId(), MinId()) # invalid start bound with pytest.raises(RequestError): await redis_client.xrange(key, IdBound("not_a_stream_id"), MaxId()) + with pytest.raises(RequestError): + await redis_client.xrevrange(key, MaxId(), IdBound("not_a_stream_id")) # invalid end bound with pytest.raises(RequestError): await redis_client.xrange(key, MinId(), IdBound("not_a_stream_id")) + with pytest.raises(RequestError): + await redis_client.xrevrange(key, IdBound("not_a_stream_id"), MinId()) @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index e4b86b1e83..9f5acb64bd 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -476,6 +476,8 @@ async def transaction_test( args.append(2) transaction.xrange(key11, IdBound("0-1"), IdBound("0-1")) args.append({"0-1": [["foo", "bar"]]}) + transaction.xrevrange(key11, IdBound("0-1"), IdBound("0-1")) + args.append({"0-1": [["foo", "bar"]]}) transaction.xtrim(key11, TrimByMinId(threshold="0-2", exact=True)) args.append(1) transaction.xdel(key11, ["0-2", "0-3"]) From 2390e7190c550e73974905f6fd2c993f644459f9 Mon Sep 17 00:00:00 2001 From: Andrew Carbonetto Date: Sun, 23 Jun 2024 18:24:24 -0700 Subject: [PATCH 04/17] Java: Add `FUNCTION DUMP` and `FUNCTION RESTORE` (#1622) * Java: Add `FUNCTION DUMP` and `FUNCTION RESTORE`. (#370) * Add `FUNCTION DUMP` and `FUNCTION RESTORE` implementations. Signed-off-by: Yury-Fridlyand * Address PR comments. Signed-off-by: Yury-Fridlyand * Add tests. Signed-off-by: Yury-Fridlyand * Address PR comments. Signed-off-by: Yury-Fridlyand --------- Signed-off-by: Yury-Fridlyand * Use GlideString Signed-off-by: Andrew Carbonetto * Clean up FUNCTION DUMP & RESTORE Signed-off-by: Andrew Carbonetto * Update handlers Signed-off-by: Andrew Carbonetto * Update comments Signed-off-by: Andrew Carbonetto * Add cluster IT test Signed-off-by: Andrew Carbonetto * quick review comment Signed-off-by: Andrew Carbonetto * SPOTLESS Signed-off-by: Andrew Carbonetto --------- Signed-off-by: Yury-Fridlyand Signed-off-by: Andrew Carbonetto Co-authored-by: Yury-Fridlyand --- glide-core/src/protobuf/redis_request.proto | 2 + glide-core/src/request_type.rs | 6 + .../src/main/java/glide/api/BaseClient.java | 18 ++- .../src/main/java/glide/api/RedisClient.java | 26 ++++ .../java/glide/api/RedisClusterClient.java | 54 +++++++ .../ScriptingAndFunctionsClusterCommands.java | 107 ++++++++++++++ .../ScriptingAndFunctionsCommands.java | 48 ++++++ .../java/glide/api/models/ClusterValue.java | 12 ++ .../function/FunctionRestorePolicy.java | 30 ++++ .../test/java/glide/api/RedisClientTest.java | 68 +++++++++ .../glide/api/RedisClusterClientTest.java | 137 ++++++++++++++++++ .../glide/api/models/ClusterValueTests.java | 21 ++- .../test/java/glide/cluster/CommandTests.java | 85 +++++++++++ .../java/glide/standalone/CommandTests.java | 71 +++++++++ 14 files changed, 679 insertions(+), 6 deletions(-) create mode 100644 java/client/src/main/java/glide/api/models/commands/function/FunctionRestorePolicy.java diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 12341ea919..1362911ffd 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -234,6 +234,8 @@ enum RequestType { Dump = 193; Restore = 194; SortReadOnly = 195; + FunctionDump = 196; + FunctionRestore = 197; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 526c14a2f4..e88f5ef8f5 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -204,6 +204,8 @@ pub enum RequestType { Dump = 193, Restore = 194, SortReadOnly = 195, + FunctionDump = 196, + FunctionRestore = 197, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -411,6 +413,8 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::Dump => RequestType::Dump, ProtobufRequestType::Restore => RequestType::Restore, ProtobufRequestType::SortReadOnly => RequestType::SortReadOnly, + ProtobufRequestType::FunctionDump => RequestType::FunctionDump, + ProtobufRequestType::FunctionRestore => RequestType::FunctionRestore, } } } @@ -616,6 +620,8 @@ impl RequestType { RequestType::Dump => Some(cmd("DUMP")), RequestType::Restore => Some(cmd("RESTORE")), RequestType::SortReadOnly => Some(cmd("SORT_RO")), + RequestType::FunctionDump => Some(get_two_word_command("FUNCTION", "DUMP")), + RequestType::FunctionRestore => Some(get_two_word_command("FUNCTION", "RESTORE")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 7167ba4bff..7f468f79d5 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -327,12 +327,18 @@ protected static CommandManager buildCommandManager(ChannelHandler channelHandle /** * Extracts the value from a GLIDE core response message and either throws an - * exception or returns the value as an object of type T. If isNullable, - * than also returns null. + * exception or returns the value as an object of type T. * * @param response Redis protobuf message. * @param classType Parameter T class type. - * @param isNullable Accepts null values in the protobuf message. + * @param flags A set of parameters which describes how to handle the response. Could be empty or + * any combination of + *
    + *
  • {@link ResponseFlags#ENCODING_UTF8} to return the data as a String; if + * unset, a byte[] is returned. + *
  • {@link ResponseFlags#IS_NULLABLE} to accept null values. + *
+ * * @return Response as an object of type T or null. * @param The return value type. * @throws RedisException On a type mismatch. @@ -436,12 +442,14 @@ protected Map handleMapResponse(Response response) throws RedisEx } /** + * Get a map and convert {@link Map} keys from byte[] to {@link String}. + * * @param response A Protobuf response * @return A map of GlideString to V. * @param Value type. */ @SuppressWarnings("unchecked") // raw Map cast to Map - protected Map handleMapResponseBinary(Response response) + protected Map handleBinaryStringMapResponse(Response response) throws RedisException { return handleRedisResponse(Map.class, EnumSet.noneOf(ResponseFlags.class), response); } @@ -737,7 +745,7 @@ public CompletableFuture> hgetall(@NonNull String key) { @Override public CompletableFuture> hgetall(@NonNull GlideString key) { return commandManager.submitNewCommand( - HGetAll, new GlideString[] {key}, this::handleMapResponseBinary); + HGetAll, new GlideString[] {key}, this::handleBinaryStringMapResponse); } @Override diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 3a748cc8de..a9e743819a 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.GlideString.gs; import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.SortOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; @@ -22,10 +23,12 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FlushDB; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDump; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionRestore; import static redis_request.RedisRequestOuterClass.RequestType.FunctionStats; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; @@ -44,10 +47,12 @@ import glide.api.commands.ScriptingAndFunctionsCommands; import glide.api.commands.ServerManagementCommands; import glide.api.commands.TransactionsCommands; +import glide.api.models.GlideString; import glide.api.models.Transaction; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.SortOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.RedisClientConfiguration; import glide.managers.CommandManager; import glide.managers.ConnectionManager; @@ -277,6 +282,27 @@ public CompletableFuture functionDelete(@NonNull String libName) { FunctionDelete, new String[] {libName}, this::handleStringResponse); } + @Override + public CompletableFuture functionDump() { + return commandManager.submitNewCommand( + FunctionDump, new GlideString[0], this::handleBytesOrNullResponse); + } + + @Override + public CompletableFuture functionRestore(byte @NonNull [] payload) { + return commandManager.submitNewCommand( + FunctionRestore, new GlideString[] {gs(payload)}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore( + byte @NonNull [] payload, @NonNull FunctionRestorePolicy policy) { + return commandManager.submitNewCommand( + FunctionRestore, + new GlideString[] {gs(payload), gs(policy.toString())}, + this::handleStringResponse); + } + @Override public CompletableFuture fcall(@NonNull String function) { return fcall(function, new String[0], new String[0]); diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index 0ac4374d87..b203879ba1 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -2,6 +2,7 @@ package glide.api; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; +import static glide.api.models.GlideString.gs; import static glide.api.models.commands.SortBaseOptions.STORE_COMMAND_STRING; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; import static glide.api.models.commands.function.FunctionListOptions.WITH_CODE_REDIS_API; @@ -24,10 +25,12 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FlushDB; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDump; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionRestore; import static redis_request.RedisRequestOuterClass.RequestType.FunctionStats; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; @@ -46,9 +49,11 @@ import glide.api.commands.TransactionsClusterCommands; import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; +import glide.api.models.GlideString; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.SortClusterOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; @@ -580,6 +585,55 @@ public CompletableFuture functionDelete(@NonNull String libName, @NonNul FunctionDelete, new String[] {libName}, route, this::handleStringResponse); } + @Override + public CompletableFuture functionDump() { + return commandManager.submitNewCommand( + FunctionDump, new GlideString[] {}, this::handleBytesOrNullResponse); + } + + @Override + public CompletableFuture> functionDump(@NonNull Route route) { + return commandManager.submitNewCommand( + FunctionDump, + new GlideString[] {}, + route, + response -> + route instanceof SingleNodeRoute + ? ClusterValue.ofSingleValue(handleBytesOrNullResponse(response)) + : ClusterValue.ofMultiValueBinary(handleBinaryStringMapResponse(response))); + } + + @Override + public CompletableFuture functionRestore(byte @NonNull [] payload) { + return commandManager.submitNewCommand( + FunctionRestore, new GlideString[] {gs(payload)}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore( + byte @NonNull [] payload, @NonNull FunctionRestorePolicy policy) { + return commandManager.submitNewCommand( + FunctionRestore, + new GlideString[] {gs(payload), gs(policy.toString())}, + this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore(byte @NonNull [] payload, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionRestore, new GlideString[] {gs(payload)}, route, this::handleStringResponse); + } + + @Override + public CompletableFuture functionRestore( + byte @NonNull [] payload, @NonNull FunctionRestorePolicy policy, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionRestore, + new GlideString[] {gs(payload), gs(policy.toString())}, + route, + this::handleStringResponse); + } + @Override public CompletableFuture fcall(@NonNull String function) { return fcall(function, new String[0]); 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 b143ec6e59..a79c6fa661 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java @@ -3,6 +3,7 @@ import glide.api.models.ClusterValue; import glide.api.models.commands.FlushMode; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.ReadFrom; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import java.util.Map; @@ -258,6 +259,8 @@ 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
@@ -267,6 +270,110 @@ CompletableFuture[]>> functionList(
      */
     CompletableFuture functionDelete(String libName, Route route);
 
+    /**
+     * Returns the serialized payload of all loaded libraries.
+ * The command will be routed to a random node. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @return The serialized payload of all loaded libraries. + * @example + *
{@code
+     * byte[] data = client.functionDump().get();
+     * // data can be used to restore loaded functions on any Redis instance
+     * }
+ */ + CompletableFuture functionDump(); + + /** + * Returns the serialized payload of all loaded 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 The serialized payload of all loaded libraries. + * @example + *
{@code
+     * byte[] data = client.functionDump(RANDOM).get().getSingleValue();
+     * // data can be used to restore loaded functions on any Redis instance
+     * }
+ */ + CompletableFuture> functionDump(Route route); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}.
+ * The command will be routed to all primary nodes. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}.
+ * The command will be routed to all primary nodes. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @param policy A policy for handling existing libraries. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data, FLUSH).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload, FunctionRestorePolicy policy); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump(Route)}. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @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.functionRestore(data, ALL_PRIMARIES).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload, Route route); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump(Route)}. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @param policy A policy for handling existing libraries. + * @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.functionRestore(data, FLUSH, ALL_PRIMARIES).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore( + byte[] payload, FunctionRestorePolicy policy, Route route); + /** * Invokes a previously loaded function.
* The command will be routed to a primary random node.
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 96d6ce0792..20a488ff69 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java @@ -2,6 +2,7 @@ package glide.api.commands; import glide.api.models.commands.FlushMode; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.ReadFrom; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -129,6 +130,53 @@ public interface ScriptingAndFunctionsCommands { */ CompletableFuture functionDelete(String libName); + /** + * Returns the serialized payload of all loaded libraries. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @return The serialized payload of all loaded libraries. + * @example + *
{@code
+     * byte[] data = client.functionDump().get();
+     * // now data could be saved to restore loaded functions on any Redis instance
+     * }
+ */ + CompletableFuture functionDump(); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload); + + /** + * Restores libraries from the serialized payload returned by {@link #functionDump()}.. + * + * @since Redis 7.0 and above. + * @see redis.io for + * details. + * @param payload The serialized data from {@link #functionDump()}. + * @param policy A policy for handling existing libraries. + * @return OK. + * @example + *
{@code
+     * String response = client.functionRestore(data, FLUSH).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionRestore(byte[] payload, FunctionRestorePolicy policy); + /** * Invokes a previously loaded function.
* This command is routed to primary nodes only.
diff --git a/java/client/src/main/java/glide/api/models/ClusterValue.java b/java/client/src/main/java/glide/api/models/ClusterValue.java index 360b2bcaa9..570834d506 100644 --- a/java/client/src/main/java/glide/api/models/ClusterValue.java +++ b/java/client/src/main/java/glide/api/models/ClusterValue.java @@ -3,6 +3,7 @@ import glide.api.models.configuration.RequestRoutingConfiguration.Route; import java.util.Map; +import java.util.stream.Collectors; /** * Represents a returned value object from a Redis server with cluster-mode enabled. The response @@ -68,6 +69,17 @@ public static ClusterValue ofMultiValue(Map data) { return res; } + /** A constructor for the value. */ + public static ClusterValue ofMultiValueBinary(Map data) { + var res = new ClusterValue(); + // the map node address can be converted to a string + Map multiValue = + data.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue)); + res.multiValue = multiValue; + return res; + } + /** * Check that multi-value is stored in this object. Should be called prior to {@link * #getMultiValue()}. diff --git a/java/client/src/main/java/glide/api/models/commands/function/FunctionRestorePolicy.java b/java/client/src/main/java/glide/api/models/commands/function/FunctionRestorePolicy.java new file mode 100644 index 0000000000..1f45131e6b --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/function/FunctionRestorePolicy.java @@ -0,0 +1,30 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.function; + +import glide.api.commands.ScriptingAndFunctionsClusterCommands; +import glide.api.commands.ScriptingAndFunctionsCommands; +import glide.api.models.configuration.RequestRoutingConfiguration.Route; + +/** + * Option for FUNCTION RESTORE command: {@link + * ScriptingAndFunctionsCommands#functionRestore(byte[], FunctionRestorePolicy)}, {@link + * ScriptingAndFunctionsClusterCommands#functionRestore(byte[], FunctionRestorePolicy)}, and {@link + * ScriptingAndFunctionsClusterCommands#functionRestore(byte[], FunctionRestorePolicy, Route)}. + * + * @see redis.io for details. + */ +public enum FunctionRestorePolicy { + /** + * Appends the restored libraries to the existing libraries and aborts on collision. This is the + * default policy. + */ + APPEND, + /** Deletes all existing libraries before restoring the payload. */ + FLUSH, + /** + * Appends the restored libraries to the existing libraries, replacing any existing ones in case + * of name collisions. Note that this policy doesn't prevent function name collisions, only + * libraries. + */ + REPLACE +} diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index d8e4014504..688fb1a612 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -98,10 +98,12 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FlushDB; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDump; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionRestore; import static redis_request.RedisRequestOuterClass.RequestType.FunctionStats; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; import static redis_request.RedisRequestOuterClass.RequestType.GeoDist; @@ -280,6 +282,7 @@ import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.function.FunctionLoadOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.commands.geospatial.GeoAddOptions; import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; @@ -5814,6 +5817,71 @@ public void functionStats_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void functionDump_returns_success() { + // setup + byte[] value = new byte[] {42}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDump), eq(new GlideString[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDump(); + byte[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionRestore_returns_success() { + // setup + byte[] data = new byte[] {42}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(FunctionRestore), eq(new GlideString[] {gs(data)}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_policy_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data), gs(FunctionRestorePolicy.FLUSH.toString())}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data, FunctionRestorePolicy.FLUSH); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void bitcount_returns_success() { diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 26a3cef828..7c1d9945a0 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.GlideString.gs; import static glide.api.models.commands.FlushMode.ASYNC; import static glide.api.models.commands.FlushMode.SYNC; import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; @@ -35,10 +36,12 @@ import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; import static redis_request.RedisRequestOuterClass.RequestType.FlushDB; import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDump; import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush; import static redis_request.RedisRequestOuterClass.RequestType.FunctionKill; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionRestore; import static redis_request.RedisRequestOuterClass.RequestType.FunctionStats; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.LastSave; @@ -52,11 +55,13 @@ import glide.api.models.ClusterTransaction; import glide.api.models.ClusterValue; +import glide.api.models.GlideString; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.SortBaseOptions.Limit; import glide.api.models.commands.SortClusterOptions; import glide.api.models.commands.function.FunctionLoadOptions; +import glide.api.models.commands.function.FunctionRestorePolicy; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute; import glide.managers.CommandManager; @@ -1871,6 +1876,138 @@ public void functionStats_with_route_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void functionDump_returns_success() { + // setup + byte[] value = new byte[] {42}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDump), eq(new GlideString[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDump(); + byte[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionDump_with_route_returns_success() { + // setup + ClusterValue value = ClusterValue.of(new byte[] {42}); + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand( + eq(FunctionDump), eq(new GlideString[0]), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.functionDump(RANDOM); + ClusterValue payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionRestore_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data)}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_policy_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data), gs(FunctionRestorePolicy.FLUSH.toString())}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data, FunctionRestorePolicy.FLUSH); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_route_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data)}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionRestore(data, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void functionRestore_with_policy_and_route_returns_success() { + // setup + byte[] data = new byte[] {42}; + GlideString[] args = {gs(data), gs(FunctionRestorePolicy.FLUSH.toString())}; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionRestore), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.functionRestore(data, FunctionRestorePolicy.FLUSH, RANDOM); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + @SneakyThrows @Test public void randomKey_with_route() { diff --git a/java/client/src/test/java/glide/api/models/ClusterValueTests.java b/java/client/src/test/java/glide/api/models/ClusterValueTests.java index f74ab21494..2954401986 100644 --- a/java/client/src/test/java/glide/api/models/ClusterValueTests.java +++ b/java/client/src/test/java/glide/api/models/ClusterValueTests.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.models.GlideString.gs; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -70,6 +71,24 @@ public void multi_value_ctor() { assertAll( () -> assertTrue(value.hasMultiData()), () -> assertFalse(value.hasSingleData()), - () -> assertNotNull(value.getMultiValue())); + () -> assertNotNull(value.getMultiValue()), + () -> assertTrue(value.getMultiValue().containsKey("config1")), + () -> assertTrue(value.getMultiValue().containsKey("config2"))); + } + + @Test + public void multi_value_binary_ctor() { + var value = + ClusterValue.ofMultiValueBinary( + Map.of(gs("config1"), gs("param1"), gs("config2"), gs("param2"))); + assertAll( + () -> assertTrue(value.hasMultiData()), + () -> assertFalse(value.hasSingleData()), + () -> assertNotNull(value.getMultiValue()), + // ofMultiValueBinary converts the key to a String, but the values are not converted + () -> assertTrue(value.getMultiValue().containsKey("config1")), + () -> assertTrue(value.getMultiValue().get("config1").equals(gs("param1"))), + () -> assertTrue(value.getMultiValue().containsKey("config2")), + () -> assertTrue(value.getMultiValue().get("config2").equals(gs("param2")))); } } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 8922482409..8ab25d34e1 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -25,6 +25,9 @@ import static glide.api.models.commands.InfoOptions.Section.STATS; import static glide.api.models.commands.ScoreFilter.MAX; import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.function.FunctionRestorePolicy.APPEND; +import static glide.api.models.commands.function.FunctionRestorePolicy.FLUSH; +import static glide.api.models.commands.function.FunctionRestorePolicy.REPLACE; import static glide.api.models.configuration.RequestRoutingConfiguration.ByAddressRoute; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; @@ -1593,6 +1596,88 @@ public void functionStats_with_route(boolean singleNodeRoute) { } } + @Test + @SneakyThrows + public void function_dump_and_restore() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + + // dumping an empty lib + byte[] emptyDump = clusterClient.functionDump().get(); + assertTrue(emptyDump.length > 0); + + String name1 = "Foster"; + String libname1 = "FosterLib"; + String name2 = "Dogster"; + String libname2 = "DogsterLib"; + + // function $name1 returns first argument + // function $name2 returns argument array len + String code = + generateLuaLibCode(libname1, Map.of(name1, "return args[1]", name2, "return #args"), true); + assertEquals(libname1, clusterClient.functionLoad(code, true).get()); + Map[] flist = clusterClient.functionList(true).get(); + + final byte[] dump = clusterClient.functionDump().get(); + + // restore without cleaning the lib and/or overwrite option causes an error + var executionException = + assertThrows(ExecutionException.class, () -> clusterClient.functionRestore(dump).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + libname1 + " already exists")); + + // APPEND policy also fails for the same reason (name collision) + executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionRestore(dump, APPEND).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + libname1 + " already exists")); + + // REPLACE policy succeeds + assertEquals(OK, clusterClient.functionRestore(dump, REPLACE).get()); + // but nothing changed - all code overwritten + var restoredFunctionList = clusterClient.functionList(true).get(); + assertEquals(1, restoredFunctionList.length); + assertEquals(libname1, restoredFunctionList[0].get("library_name")); + // Note that function ordering may differ across nodes so we can't do a deep equals + assertEquals(2, ((Object[]) restoredFunctionList[0].get("functions")).length); + + // create lib with another name, but with the same function names + assertEquals(OK, clusterClient.functionFlush(SYNC).get()); + code = + generateLuaLibCode(libname2, Map.of(name1, "return args[1]", name2, "return #args"), true); + assertEquals(libname2, clusterClient.functionLoad(code, true).get()); + restoredFunctionList = clusterClient.functionList(true).get(); + assertEquals(1, restoredFunctionList.length); + assertEquals(libname2, restoredFunctionList[0].get("library_name")); + + // REPLACE policy now fails due to a name collision + executionException = + assertThrows( + ExecutionException.class, () -> clusterClient.functionRestore(dump, REPLACE).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + // redis checks names in random order and blames on first collision + assertTrue( + executionException.getMessage().contains("Function " + name1 + " already exists") + || executionException.getMessage().contains("Function " + name2 + " already exists")); + + // FLUSH policy succeeds, but deletes the second lib + assertEquals(OK, clusterClient.functionRestore(dump, FLUSH).get()); + restoredFunctionList = clusterClient.functionList(true).get(); + assertEquals(1, restoredFunctionList.length); + assertEquals(libname1, restoredFunctionList[0].get("library_name")); + // Note that function ordering may differ across nodes + assertEquals(2, ((Object[]) restoredFunctionList[0].get("functions")).length); + + // call restored functions + assertEquals( + "meow", + clusterClient.fcallReadOnly(name1, new String[0], new String[] {"meow", "woem"}).get()); + assertEquals( + 2L, clusterClient.fcallReadOnly(name2, new String[0], new String[] {"meow", "woem"}).get()); + } + @Test @SneakyThrows public void randomKey() { diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index d18642e058..42bc850322 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -2,6 +2,7 @@ package glide.standalone; import static glide.TestConfiguration.REDIS_VERSION; +import static glide.TestUtilities.assertDeepEquals; import static glide.TestUtilities.checkFunctionListResponse; import static glide.TestUtilities.checkFunctionStatsResponse; import static glide.TestUtilities.commonClientConfig; @@ -21,6 +22,9 @@ import static glide.api.models.commands.SortBaseOptions.Limit; import static glide.api.models.commands.SortBaseOptions.OrderBy.ASC; import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC; +import static glide.api.models.commands.function.FunctionRestorePolicy.APPEND; +import static glide.api.models.commands.function.FunctionRestorePolicy.FLUSH; +import static glide.api.models.commands.function.FunctionRestorePolicy.REPLACE; import static glide.cluster.CommandTests.DEFAULT_INFO_SECTIONS; import static glide.cluster.CommandTests.EVERYTHING_INFO_SECTIONS; import static org.junit.jupiter.api.Assertions.assertAll; @@ -727,6 +731,73 @@ public void functionStats() { checkFunctionStatsResponse(response, new String[0], 0, 0); } + @Test + @SneakyThrows + public void function_dump_and_restore() { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + + // dumping an empty lib + byte[] emptyDump = regularClient.functionDump().get(); + assertTrue(emptyDump.length > 0); + + String name1 = "Foster"; + String name2 = "Dogster"; + + // function $name1 returns first argument + // function $name2 returns argument array len + String code = + generateLuaLibCode(name1, Map.of(name1, "return args[1]", name2, "return #args"), false); + assertEquals(name1, regularClient.functionLoad(code, true).get()); + var flist = regularClient.functionList(true).get(); + + final byte[] dump = regularClient.functionDump().get(); + + // restore without cleaning the lib and/or overwrite option causes an error + var executionException = + assertThrows(ExecutionException.class, () -> regularClient.functionRestore(dump).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + name1 + " already exists")); + + // APPEND policy also fails for the same reason (name collision) + executionException = + assertThrows( + ExecutionException.class, () -> regularClient.functionRestore(dump, APPEND).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + assertTrue(executionException.getMessage().contains("Library " + name1 + " already exists")); + + // REPLACE policy succeeds + assertEquals(OK, regularClient.functionRestore(dump, REPLACE).get()); + // but nothing changed - all code overwritten + assertDeepEquals(flist, regularClient.functionList(true).get()); + + // create lib with another name, but with the same function names + assertEquals(OK, regularClient.functionFlush(SYNC).get()); + code = generateLuaLibCode(name2, Map.of(name1, "return args[1]", name2, "return #args"), false); + assertEquals(name2, regularClient.functionLoad(code, true).get()); + + // REPLACE policy now fails due to a name collision + executionException = + assertThrows( + ExecutionException.class, () -> regularClient.functionRestore(dump, REPLACE).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + // redis checks names in random order and blames on first collision + assertTrue( + executionException.getMessage().contains("Function " + name1 + " already exists") + || executionException.getMessage().contains("Function " + name2 + " already exists")); + + // FLUSH policy succeeds, but deletes the second lib + assertEquals(OK, regularClient.functionRestore(dump, FLUSH).get()); + assertDeepEquals(flist, regularClient.functionList(true).get()); + + // call restored functions + assertEquals( + "meow", regularClient.fcall(name1, new String[0], new String[] {"meow", "woem"}).get()); + assertEquals( + 2L, regularClient.fcall(name2, new String[0], new String[] {"meow", "woem"}).get()); + } + @SneakyThrows @Test public void randomkey() { From 93b0f02f90b6081c68ab0c5781fcaddaaf86a4c0 Mon Sep 17 00:00:00 2001 From: eifrah-aws Date: Mon, 24 Jun 2024 14:28:42 +0300 Subject: [PATCH 05/17] Added support for `append` + `smembers` (`GlideString` version) (#1629) --- .../src/main/java/glide/api/BaseClient.java | 17 +++++++++++++++++ .../glide/api/commands/SetBaseCommands.java | 16 ++++++++++++++++ .../api/commands/StringBaseCommands.java | 17 +++++++++++++++++ .../test/java/glide/SharedCommandTests.java | 19 +++++++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 7f468f79d5..aeb8fca84c 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -487,6 +487,11 @@ protected Set handleSetResponse(Response response) throws RedisException return handleRedisResponse(Set.class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); } + @SuppressWarnings("unchecked") + protected Set handleSetBinaryResponse(Response response) throws RedisException { + return handleRedisResponse(Set.class, EnumSet.noneOf(ResponseFlags.class), response); + } + /** Process a FUNCTION LIST standalone response. */ @SuppressWarnings("unchecked") protected Map[] handleFunctionListResponse(Object[] response) { @@ -584,6 +589,12 @@ public CompletableFuture append(@NonNull String key, @NonNull String value Append, new String[] {key, value}, this::handleLongResponse); } + @Override + public CompletableFuture append(@NonNull GlideString key, @NonNull GlideString value) { + return commandManager.submitNewCommand( + Append, new GlideString[] {key, value}, this::handleLongResponse); + } + @Override public CompletableFuture mget(@NonNull String[] keys) { return commandManager.submitNewCommand( @@ -929,6 +940,12 @@ public CompletableFuture> smembers(@NonNull String key) { return commandManager.submitNewCommand(SMembers, new String[] {key}, this::handleSetResponse); } + @Override + public CompletableFuture> smembers(@NonNull GlideString key) { + return commandManager.submitNewCommand( + SMembers, new GlideString[] {key}, this::handleSetBinaryResponse); + } + @Override public CompletableFuture scard(@NonNull String key) { return commandManager.submitNewCommand(SCard, new String[] {key}, this::handleLongResponse); diff --git a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java index e94a8acfd4..11ea5d24f2 100644 --- a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -65,6 +66,21 @@ public interface SetBaseCommands { */ CompletableFuture> smembers(String key); + /** + * Retrieves all the members of the set value stored at key. + * + * @see redis.io for details. + * @param key The key from which to retrieve the set members. + * @return A Set of all members of the set. + * @remarks If key does not exist an empty set will be returned. + * @example + *
{@code
+     * Set result = client.smembers(gs("my_set")).get();
+     * assert result.equals(Set.of(gs("member1"), gs("member2"), gs("member3")));
+     * }
+ */ + CompletableFuture> smembers(GlideString key); + /** * Retrieves the set cardinality (number of elements) of the set stored at key. * diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java index 3732db4b1a..22252b3618 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -431,6 +431,23 @@ public interface StringBaseCommands { */ CompletableFuture append(String key, String value); + /** + * Appends a value to a key. If key does not exist it is + * created and set as an empty string, so APPEND will be similar to {@see #set} in + * this special case. + * + * @see redis.io for details. + * @param key The key of the string. + * @param value The value to append. + * @return The length of the string after appending the value. + * @example + *
{@code
+     * Long value = client.append(gs("key"), gs("value")).get();
+     * assert value.equals(5L);
+     * }
+ */ + CompletableFuture append(GlideString key, GlideString value); + /** * Returns the longest common subsequence between strings stored at key1 and * key2. diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 5f5d97ec73..5bb762613f 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -213,6 +213,21 @@ public void append(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void appendBinary(BaseClient client) { + GlideString key = gs(UUID.randomUUID().toString()); + GlideString value = gs(String.valueOf(UUID.randomUUID())); + + // Append on non-existing string(similar to SET) + assertEquals(value.getString().length(), client.append(key, value).get()); + + assertEquals(value.getString().length() * 2L, client.append(key, value).get()); + GlideString value2 = gs(value.getString() + value.getString()); + assertEquals(value2, client.get(key).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -1269,6 +1284,10 @@ public void sadd_srem_scard_smembers_existing_set(BaseClient client) { Set expectedMembers = Set.of("member1", "member2", "member4"); assertEquals(expectedMembers, client.smembers(key).get()); + + Set expectedMembersBin = Set.of(gs("member1"), gs("member2"), gs("member4")); + assertEquals(expectedMembersBin, client.smembers(gs(key)).get()); + assertEquals(1, client.srem(key, new String[] {"member1"}).get()); assertEquals(2, client.scard(key).get()); } From 8f03ac99a2fc294a1428cc5d5941599f9411aab6 Mon Sep 17 00:00:00 2001 From: Alon Arenberg <93711356+alon-arenberg@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:32:02 +0300 Subject: [PATCH 06/17] support bitcount with GlideString (#1627) support bitcount with GlideString --- .../src/main/java/glide/api/BaseClient.java | 29 ++++++++ .../api/commands/BitmapBaseCommands.java | 64 +++++++++++++++++ .../test/java/glide/api/RedisClientTest.java | 69 +++++++++++++++++++ 3 files changed, 162 insertions(+) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index aeb8fca84c..a4630b310f 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -1785,6 +1785,12 @@ public CompletableFuture bitcount(@NonNull String key) { return commandManager.submitNewCommand(BitCount, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture bitcount(@NonNull GlideString key) { + return commandManager.submitNewCommand( + BitCount, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture bitcount(@NonNull String key, long start, long end) { return commandManager.submitNewCommand( @@ -1793,6 +1799,16 @@ public CompletableFuture bitcount(@NonNull String key, long start, long en this::handleLongResponse); } + @Override + public CompletableFuture bitcount(@NonNull GlideString key, long start, long end) { + return commandManager.submitNewCommand( + BitCount, + new GlideString[] { + key, gs(Long.toString(start).getBytes()), gs(Long.toString(end).getBytes()) + }, + this::handleLongResponse); + } + @Override public CompletableFuture bitcount( @NonNull String key, long start, long end, @NonNull BitmapIndexType options) { @@ -1801,6 +1817,19 @@ public CompletableFuture bitcount( return commandManager.submitNewCommand(BitCount, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitcount( + @NonNull GlideString key, long start, long end, @NonNull BitmapIndexType options) { + GlideString[] arguments = + new GlideString[] { + key, + gs(Long.toString(start).getBytes()), + gs(Long.toString(end).getBytes()), + gs(options.toString().getBytes()) + }; + return commandManager.submitNewCommand(BitCount, arguments, this::handleLongResponse); + } + @Override public CompletableFuture setbit(@NonNull String key, long offset, long value) { String[] arguments = new String[] {key, Long.toString(offset), Long.toString(value)}; 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 508cff39ca..4c11660743 100644 --- a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java @@ -4,6 +4,7 @@ import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.GlideString; import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldIncrby; import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow; @@ -37,6 +38,21 @@ public interface BitmapBaseCommands { */ CompletableFuture bitcount(String key); + /** + * Counts the number of set bits (population counting) in a string stored at key. + * + * @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. + * @example + *
{@code
+     * Long payload = client.bitcount(gs("myKey1")).get();
+     * assert payload == 2L; // The string stored at "myKey1" contains 2 set bits.
+     * }
+ */ + CompletableFuture bitcount(GlideString key); + /** * Counts the number of set bits (population counting) in a string stored at key. The * offsets start and end are zero-based indexes, with 0 @@ -59,6 +75,28 @@ public interface BitmapBaseCommands { */ CompletableFuture bitcount(String key, long start, long end); + /** + * Counts the number of set bits (population counting) in a string stored at key. The + * offsets start and end are zero-based indexes, with 0 + * being the first element of the list, 1 being the next element and so on. These + * offsets can also be negative numbers indicating offsets starting at the end of the list, with + * -1 being the last element of the list, -2 being the penultimate, and + * so on. + * + * @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. + * @return The number of set bits in the string byte interval specified by start and + * end. Returns zero if the key is missing as it is treated as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount(gs("myKey1"), 1, 3).get();
+     * assert payload == 2L; // The second to fourth bytes of the string stored at "myKey1" contains 2 set bits.
+     * }
+ */ + CompletableFuture bitcount(GlideString key, long start, long end); + /** * Counts the number of set bits (population counting) in a string stored at key. The * offsets start and end are zero-based indexes, with 0 @@ -85,6 +123,32 @@ public interface BitmapBaseCommands { */ CompletableFuture bitcount(String key, long start, long end, BitmapIndexType options); + /** + * Counts the number of set bits (population counting) in a string stored at key. The + * offsets start and end are zero-based indexes, with 0 + * being the first element of the list, 1 being the next element and so on. These + * offsets can also be negative numbers indicating offsets starting at the end of the list, with + * -1 being the last element of the list, -2 being the penultimate, and + * so on. + * + * @since Redis 7.0 and above + * @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. + * @param options The index offset type. Could be either {@link BitmapIndexType#BIT} or {@link + * BitmapIndexType#BYTE}. + * @return The number of set bits in the string interval specified by start, + * end, and options. Returns zero if the key is missing as it is treated + * as an empty string. + * @example + *
{@code
+     * Long payload = client.bitcount(gs("myKey1"), 1, 1, BIT).get();
+     * assert payload == 1L; // Indicates that the second bit of the string stored at "myKey1" is set.
+     * }
+ */ + CompletableFuture bitcount(GlideString key, long start, long end, BitmapIndexType options); + /** * Sets or clears the bit at offset in the string value stored at key. * The offset is a zero-based index, with 0 being the first element of diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 688fb1a612..7011dc981b 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -5905,6 +5905,29 @@ public void bitcount_returns_success() { assertEquals(bitcount, payload); } + @SneakyThrows + @Test + public void bitcount_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitCount), eq(new GlideString[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(1L, payload); + assertEquals(bitcount, payload); + } + @SneakyThrows @Test public void bitcount_indices_returns_success() { @@ -5928,6 +5951,29 @@ public void bitcount_indices_returns_success() { assertEquals(bitcount, payload); } + @SneakyThrows + @Test + public void bitcount_indices_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(BitCount), eq(new GlideString[] {key, gs("1"), gs("2")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key, 1, 2); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitcount, payload); + } + @SneakyThrows @Test public void bitcount_indices_with_option_returns_success() { @@ -5951,6 +5997,29 @@ public void bitcount_indices_with_option_returns_success() { assertEquals(bitcount, payload); } + @SneakyThrows + @Test + public void bitcount_indices_with_option_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bitcount = 1L; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitcount); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(BitCount), eq(new GlideString[] {key, gs("1"), gs("2"), gs("BIT")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitcount(key, 1, 2, BitmapIndexType.BIT); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitcount, payload); + } + @SneakyThrows @Test public void setbit_returns_success() { From 62da7f89c72bb9a1dc34eef6ca96f64f6ee2796e Mon Sep 17 00:00:00 2001 From: Alon Arenberg <93711356+alon-arenberg@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:43:13 +0300 Subject: [PATCH 07/17] support bitop, bitpos and xdel with GlideString (#1628) support bitop, bitpos and xdel with GlideString --- .../src/main/java/glide/api/BaseClient.java | 58 +++++++++ .../api/commands/BitmapBaseCommands.java | 123 ++++++++++++++++++ .../api/commands/StreamBaseCommands.java | 18 +++ .../test/java/glide/api/RedisClientTest.java | 85 ++++++++++++ 4 files changed, 284 insertions(+) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index a4630b310f..aeaa1bcc4a 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -1488,6 +1488,12 @@ public CompletableFuture xdel(@NonNull String key, @NonNull String[] ids) return commandManager.submitNewCommand(XDel, arguments, this::handleLongResponse); } + @Override + public CompletableFuture xdel(@NonNull GlideString key, @NonNull GlideString[] ids) { + GlideString[] arguments = ArrayUtils.addFirst(ids, key); + return commandManager.submitNewCommand(XDel, arguments, this::handleLongResponse); + } + @Override public CompletableFuture> xrange( @NonNull String key, @NonNull StreamRange start, @NonNull StreamRange end) { @@ -1848,12 +1854,27 @@ public CompletableFuture bitpos(@NonNull String key, long bit) { return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitpos(@NonNull GlideString key, long bit) { + GlideString[] arguments = new GlideString[] {key, gs(Long.toString(bit).getBytes())}; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + @Override public CompletableFuture bitpos(@NonNull String key, long bit, long start) { String[] arguments = new String[] {key, Long.toString(bit), Long.toString(start)}; return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitpos(@NonNull GlideString key, long bit, long start) { + GlideString[] arguments = + new GlideString[] { + key, gs(Long.toString(bit).getBytes()), gs(Long.toString(start).getBytes()) + }; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + @Override public CompletableFuture bitpos(@NonNull String key, long bit, long start, long end) { String[] arguments = @@ -1861,6 +1882,18 @@ public CompletableFuture bitpos(@NonNull String key, long bit, long start, return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitpos(@NonNull GlideString key, long bit, long start, long end) { + GlideString[] arguments = + new GlideString[] { + key, + gs(Long.toString(bit).getBytes()), + gs(Long.toString(start).getBytes()), + gs(Long.toString(end).getBytes()) + }; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + @Override public CompletableFuture bitpos( @NonNull String key, long bit, long start, long end, @NonNull BitmapIndexType options) { @@ -1871,6 +1904,20 @@ public CompletableFuture bitpos( return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitpos( + @NonNull GlideString key, long bit, long start, long end, @NonNull BitmapIndexType options) { + GlideString[] arguments = + new GlideString[] { + key, + gs(Long.toString(bit).getBytes()), + gs(Long.toString(start).getBytes()), + gs(Long.toString(end).getBytes()), + gs(options.toString().getBytes()) + }; + return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); + } + @Override public CompletableFuture bitop( @NonNull BitwiseOperation bitwiseOperation, @@ -1881,6 +1928,17 @@ public CompletableFuture bitop( return commandManager.submitNewCommand(BitOp, arguments, this::handleLongResponse); } + @Override + public CompletableFuture bitop( + @NonNull BitwiseOperation bitwiseOperation, + @NonNull GlideString destination, + @NonNull GlideString[] keys) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(bitwiseOperation.toString().getBytes()), destination}, keys); + return commandManager.submitNewCommand(BitOp, arguments, this::handleLongResponse); + } + @Override public CompletableFuture> lmpop( @NonNull String[] keys, @NonNull ListDirection direction, long count) { 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 4c11660743..f2192ab27e 100644 --- a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java @@ -208,6 +208,25 @@ public interface BitmapBaseCommands { */ CompletableFuture bitpos(String key, long bit); + /** + * Returns the position of the first bit matching the given bit value. + * + * @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 + * the string held at key. If bit is not found, a -1 is + * returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 0).get();
+     * // Indicates that the first occurrence of a 0 bit value is the fourth bit of the binary value
+     * // of the string stored at "myKey1".
+     * assert payload == 3L;
+     * }
+ */ + CompletableFuture bitpos(GlideString key, long bit); + /** * Returns the position of the first bit matching the given bit value. The offset * start is a zero-based index, with 0 being the first byte of the list, @@ -232,6 +251,30 @@ public interface BitmapBaseCommands { */ CompletableFuture bitpos(String key, long bit, long start); + /** + * Returns the position of the first bit matching the given bit value. The offset + * start is a zero-based index, with 0 being the first byte of the list, + * 1 being the next byte and so on. These offsets can also be 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 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. + * @return The position of the first occurrence beginning at the start offset of the + * bit in the binary value of the string held at key. If bit + * is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 1, 4).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from fifth byte is the 34th
+     * // bit of the binary value of the string stored at "myKey1".
+     * assert payload == 33L;
+     * }
+ */ + CompletableFuture bitpos(GlideString key, long bit, long start); + /** * Returns the position of the first bit matching the given bit value. The offsets * start and end are zero-based indexes, with 0 being the @@ -257,6 +300,31 @@ public interface BitmapBaseCommands { */ CompletableFuture bitpos(String key, long bit, long start, long end); + /** + * Returns the position of the first bit matching the given bit value. The offsets + * start and end are zero-based indexes, with 0 being the + * first byte of the list, 1 being the next byte and so on. These offsets can also be + * 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 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. + * @param end The ending offset. + * @return The position of the first occurrence from the start to the end + * offsets of the bit in the binary value of the string held at key + * . If bit is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 1, 4, 6).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from the fifth to seventh
+     * // bytes is the 34th bit of the binary value of the string stored at "myKey1".
+     * assert payload == 33L;
+     * }
+ */ + CompletableFuture bitpos(GlideString key, long bit, long start, long end); + /** * Returns the position of the first bit matching the given bit value. The offset * offsetType specifies whether the offset is a BIT or BYTE. If BIT is specified, @@ -289,6 +357,38 @@ public interface BitmapBaseCommands { CompletableFuture bitpos( String key, long bit, long start, long end, BitmapIndexType offsetType); + /** + * Returns the position of the first bit matching the given bit value. The offset + * offsetType specifies whether the offset is a BIT or BYTE. If BIT is specified, + * start==0 and end==2 means to look at the first three bits. If BYTE is + * specified, start==0 and end==2 means to look at the first three bytes + * The offsets are zero-based indexes, with 0 being the first element of the list, + * 1 being the next, and so on. These offsets can also be negative numbers indicating + * offsets starting at the end of the list, with -1 being the last element of the + * list, -2 being the penultimate, and so on. + * + * @since Redis 7.0 and above. + * @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. + * @param end The ending offset. + * @param offsetType The index offset type. Could be either {@link BitmapIndexType#BIT} or {@link + * BitmapIndexType#BYTE}. + * @return The position of the first occurrence from the start to the end + * offsets of the bit in the binary value of the string held at key + * . If bit is not found, a -1 is returned. + * @example + *
{@code
+     * Long payload = client.bitpos(gs("myKey1"), 1, 4, 6, BIT).get();
+     * // Indicates that the first occurrence of a 1 bit value starting from the fifth to seventh
+     * // bits is the sixth bit of the binary value of the string stored at "myKey1".
+     * assert payload == 5L;
+     * }
+ */ + CompletableFuture bitpos( + GlideString key, long bit, long start, long end, BitmapIndexType offsetType); + /** * Perform a bitwise operation between multiple keys (containing string values) and store the * result in the destination. @@ -312,6 +412,29 @@ CompletableFuture bitpos( CompletableFuture bitop( BitwiseOperation bitwiseOperation, String destination, String[] keys); + /** + * Perform a bitwise operation between multiple keys (containing string values) and store the + * result in the destination. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @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. + * @return The size of the string stored in destination. + * @example + *
{@code
+     * client.set("key1", "A"); // "A" has binary value 01000001
+     * client.set("key2", "B"); // "B" has binary value 01000010
+     * Long payload = client.bitop(BitwiseOperation.AND, gs("destination"), new GlideString[] {key1, key2}).get();
+     * assert "@".equals(client.get("destination").get()); // "@" has binary value 01000000
+     * assert payload == 1L; // The size of the resulting string is 1.
+     * }
+ */ + CompletableFuture bitop( + BitwiseOperation bitwiseOperation, GlideString destination, GlideString[] keys); + /** * Reads or modifies the array of bits representing the string that is held at key * based on the specified subCommands. diff --git a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java index b49c6472c6..3a2b200939 100644 --- a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.commands.stream.StreamAddOptions; import glide.api.models.commands.stream.StreamAddOptions.StreamAddOptionsBuilder; import glide.api.models.commands.stream.StreamGroupOptions; @@ -168,6 +169,23 @@ CompletableFuture>> xread( */ CompletableFuture xdel(String key, String[] ids); + /** + * Removes the specified entries by id from a stream, and returns the number of entries deleted. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @param ids An array of entry ids. + * @return The number of entries removed from the stream. This number may be less than the number + * of entries in ids, if the specified ids don't exist in the + * stream. + * @example + *
{@code
+     * Long num = client.xdel("key", new GlideString[] {gs("1538561698944-0"), gs("1538561698944-1")}).get();
+     * assert num == 2L; // Stream marked 2 entries as deleted
+     * }
+ */ + CompletableFuture xdel(GlideString key, GlideString[] ids); + /** * Returns stream entries matching a given range of IDs. * diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 7011dc981b..3703644e3e 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -4330,6 +4330,31 @@ public void xdel_returns_success() { assertEquals(completedResult, payload); } + @Test + @SneakyThrows + public void xdel_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] ids = {gs("one-1"), gs("two-2"), gs("three-3")}; + Long completedResult = 69L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(completedResult); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(XDel), eq(new GlideString[] {key, gs("one-1"), gs("two-2"), gs("three-3")}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.xdel(key, ids); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(completedResult, payload); + } + @Test @SneakyThrows public void xrange_returns_success() { @@ -6242,6 +6267,39 @@ public void bitpos_with_start_and_end_and_type_returns_success() { assertEquals(bitPosition, payload); } + @SneakyThrows + @Test + public void bitpos_with_start_and_end_and_type_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + Long bit = 0L; + Long start = 5L; + Long end = 10L; + Long bitPosition = 10L; + GlideString[] arguments = + new GlideString[] { + key, + gs(Long.toString(bit).getBytes()), + gs(Long.toString(start).getBytes()), + gs(Long.toString(end).getBytes()), + gs(BitmapIndexType.BIT.toString().getBytes()) + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(bitPosition); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitPos), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitpos(key, bit, start, end, BitmapIndexType.BIT); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(bitPosition, payload); + } + @SneakyThrows @Test public void bitop_returns_success() { @@ -6267,6 +6325,33 @@ public void bitop_returns_success() { assertEquals(result, payload); } + @SneakyThrows + @Test + public void bitop_bianry_returns_success() { + // setup + GlideString destination = gs("destination"); + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + Long result = 6L; + BitwiseOperation bitwiseAnd = BitwiseOperation.AND; + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(bitwiseAnd.toString().getBytes()), destination}, keys); + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitOp), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.bitop(bitwiseAnd, destination, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + @SneakyThrows @Test public void lmpop_returns_success() { From 6242abbf9576a51dffaa9934c15d8c346ade1cd5 Mon Sep 17 00:00:00 2001 From: Alon Arenberg <93711356+alon-arenberg@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:43:38 +0300 Subject: [PATCH 08/17] support hincrby, hincrbyfloat, incrby and incrbyfloat with GlideString (#1630) support hincrby, hincrbyfloat, incrby and incrbyfloat with GlideString --- .../src/main/java/glide/api/BaseClient.java | 34 ++++++ .../glide/api/commands/HashBaseCommands.java | 45 ++++++++ .../api/commands/StringBaseCommands.java | 34 ++++++ .../test/java/glide/api/RedisClientTest.java | 108 ++++++++++++++++++ 4 files changed, 221 insertions(+) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index aeaa1bcc4a..07a050f405 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -662,12 +662,28 @@ public CompletableFuture incrBy(@NonNull String key, long amount) { IncrBy, new String[] {key, Long.toString(amount)}, this::handleLongResponse); } + @Override + public CompletableFuture incrBy(@NonNull GlideString key, long amount) { + return commandManager.submitNewCommand( + IncrBy, + new GlideString[] {key, gs(Long.toString(amount).getBytes())}, + this::handleLongResponse); + } + @Override public CompletableFuture incrByFloat(@NonNull String key, double amount) { return commandManager.submitNewCommand( IncrByFloat, new String[] {key, Double.toString(amount)}, this::handleDoubleResponse); } + @Override + public CompletableFuture incrByFloat(@NonNull GlideString key, double amount) { + return commandManager.submitNewCommand( + IncrByFloat, + new GlideString[] {key, gs(Double.toString(amount).getBytes())}, + this::handleDoubleResponse); + } + @Override public CompletableFuture decr(@NonNull String key) { return commandManager.submitNewCommand(Decr, new String[] {key}, this::handleLongResponse); @@ -765,6 +781,15 @@ public CompletableFuture hincrBy(@NonNull String key, @NonNull String fiel HIncrBy, new String[] {key, field, Long.toString(amount)}, this::handleLongResponse); } + @Override + public CompletableFuture hincrBy( + @NonNull GlideString key, @NonNull GlideString field, long amount) { + return commandManager.submitNewCommand( + HIncrBy, + new GlideString[] {key, field, gs(Long.toString(amount).getBytes())}, + this::handleLongResponse); + } + @Override public CompletableFuture hincrByFloat( @NonNull String key, @NonNull String field, double amount) { @@ -774,6 +799,15 @@ public CompletableFuture hincrByFloat( this::handleDoubleResponse); } + @Override + public CompletableFuture hincrByFloat( + @NonNull GlideString key, @NonNull GlideString field, double amount) { + return commandManager.submitNewCommand( + HIncrByFloat, + new GlideString[] {key, field, gs(Double.toString(amount).getBytes())}, + this::handleDoubleResponse); + } + @Override public CompletableFuture hkeys(@NonNull String key) { return commandManager.submitNewCommand( diff --git a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java index 3771d3b3a9..c39d3bc437 100644 --- a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java @@ -216,6 +216,28 @@ public interface HashBaseCommands { */ CompletableFuture hincrBy(String key, String field, long amount); + /** + * Increments the number stored at field in the hash stored at key by + * increment. By using a negative increment value, the value stored at field in the + * hash stored at key is decremented. If field or key does + * not exist, it is set to 0 before performing the operation. + * + * @see redis.io for details. + * @param key The key of the hash. + * @param field The field in the hash stored at key to increment or decrement its + * value. + * @param amount The amount by which to increment or decrement the field's value. Use a negative + * value to decrement. + * @return The value of field in the hash stored at key after the + * increment or decrement. + * @example + *
{@code
+     * Long num = client.hincrBy(gs("my_hash"), gs("field1"), 5).get();
+     * assert num == 5L;
+     * }
+ */ + CompletableFuture hincrBy(GlideString key, GlideString field, long amount); + /** * Increments the string representing a floating point number stored at field in the * hash stored at key by increment. By using a negative increment value, the value @@ -239,6 +261,29 @@ public interface HashBaseCommands { */ CompletableFuture hincrByFloat(String key, String field, double amount); + /** + * Increments the string representing a floating point number stored at field in the + * hash stored at key by increment. By using a negative increment value, the value + * stored at field in the hash stored at key is decremented. If + * field or key does not exist, it is set to 0 before performing the + * operation. + * + * @see redis.io for details. + * @param key The key of the hash. + * @param field The field in the hash stored at key to increment or decrement its + * value. + * @param amount The amount by which to increment or decrement the field's value. Use a negative + * value to decrement. + * @return The value of field in the hash stored at key after the + * increment or decrement. + * @example + *
{@code
+     * Double num = client.hincrByFloat(gs("my_hash"), gs("field1"), 2.5).get();
+     * assert num == 2.5;
+     * }
+ */ + CompletableFuture hincrByFloat(GlideString key, GlideString field, double amount); + /** * Returns all field names in the hash stored at key. * diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java index 22252b3618..3c7b1d899b 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -301,6 +301,22 @@ public interface StringBaseCommands { */ CompletableFuture incrBy(String key, long amount); + /** + * Increments the number stored at key by amount. If key + * does not exist, it is set to 0 before performing the operation. + * + * @see redis.io for details. + * @param key The key to increment its value. + * @param amount The amount to increment. + * @return The value of key after the increment. + * @example + *
{@code
+     * Long num = client.incrBy(gs("key"), 2).get();
+     * assert num == 7L;
+     * }
+ */ + CompletableFuture incrBy(GlideString key, long amount); + /** * Increments the string representing a floating point number stored at key by * amount. By using a negative increment value, the result is that the value stored at @@ -319,6 +335,24 @@ public interface StringBaseCommands { */ CompletableFuture incrByFloat(String key, double amount); + /** + * Increments the string representing a floating point number stored at key by + * amount. By using a negative increment value, the result is that the value stored at + * key is decremented. If key does not exist, it is set to 0 before + * performing the operation. + * + * @see redis.io for details. + * @param key The key to increment its value. + * @param amount The amount to increment. + * @return The value of key after the increment. + * @example + *
{@code
+     * Double num = client.incrByFloat(gs("key"), 0.5).get();
+     * assert num == 7.5;
+     * }
+ */ + CompletableFuture incrByFloat(GlideString key, double amount); + /** * Decrements the number stored at key by one. If key does not exist, it * is set to 0 before performing the operation. diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 3703644e3e..29a1119943 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -1227,6 +1227,31 @@ public void incrBy_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void incrBy_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(IncrBy), eq(new GlideString[] {key, gs(Long.toString(amount).getBytes())}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incrBy(key, amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void incrByFloat_returns_success() { @@ -1252,6 +1277,33 @@ public void incrByFloat_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void incrByFloat_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + double amount = 1.1; + Double value = 10.1; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(IncrByFloat), + eq(new GlideString[] {key, gs(Double.toString(amount).getBytes())}), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.incrByFloat(key, amount); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void decr_returns_success() { @@ -1620,6 +1672,34 @@ public void hincrBy_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void hincrBy_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("field"); + long amount = 1L; + Long value = 10L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(HIncrBy), + eq(new GlideString[] {key, field, gs(Long.toString(amount).getBytes())}), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hincrBy(key, field, amount); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void hincrByFloat_returns_success() { @@ -1646,6 +1726,34 @@ public void hincrByFloat_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void hincrByFloat_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString field = gs("field"); + double amount = 1.0; + Double value = 10.0; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(HIncrByFloat), + eq(new GlideString[] {key, field, gs(Double.toString(amount).getBytes())}), + any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hincrByFloat(key, field, amount); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void hkeys_returns_success() { From a41887a0ed7d2d61705f42a931d2be3d1d3cc23a Mon Sep 17 00:00:00 2001 From: Alon Arenberg <93711356+alon-arenberg@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:48:36 +0300 Subject: [PATCH 09/17] support llen, strlen, xlen and hstrlen with GlideString (#1632) support llen, strlen, xlen and hstrlen with GlideString --- .../src/main/java/glide/api/BaseClient.java | 22 +++++++++++++++++++ .../glide/api/commands/HashBaseCommands.java | 17 ++++++++++++++ .../glide/api/commands/ListBaseCommands.java | 17 ++++++++++++++ .../api/commands/StreamBaseCommands.java | 15 +++++++++++++ .../api/commands/StringBaseCommands.java | 20 +++++++++++++++++ 5 files changed, 91 insertions(+) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 07a050f405..f9303df43d 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -700,6 +700,12 @@ public CompletableFuture strlen(@NonNull String key) { return commandManager.submitNewCommand(Strlen, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture strlen(@NonNull GlideString key) { + return commandManager.submitNewCommand( + Strlen, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture setrange(@NonNull String key, int offset, @NonNull String value) { String[] arguments = new String[] {key, Integer.toString(offset), value}; @@ -822,6 +828,12 @@ public CompletableFuture hstrlen(@NonNull String key, @NonNull String fiel HStrlen, new String[] {key, field}, this::handleLongResponse); } + @Override + public CompletableFuture hstrlen(@NonNull GlideString key, @NonNull GlideString field) { + return commandManager.submitNewCommand( + HStrlen, new GlideString[] {key, field}, this::handleLongResponse); + } + @Override public CompletableFuture hrandfield(@NonNull String key) { return commandManager.submitNewCommand( @@ -925,6 +937,11 @@ public CompletableFuture llen(@NonNull String key) { return commandManager.submitNewCommand(LLen, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture llen(@NonNull GlideString key) { + return commandManager.submitNewCommand(LLen, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture lrem(@NonNull String key, long count, @NonNull String element) { return commandManager.submitNewCommand( @@ -1516,6 +1533,11 @@ public CompletableFuture xlen(@NonNull String key) { return commandManager.submitNewCommand(XLen, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture xlen(@NonNull GlideString key) { + return commandManager.submitNewCommand(XLen, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture xdel(@NonNull String key, @NonNull String[] ids) { String[] arguments = ArrayUtils.addFirst(ids, key); diff --git a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java index c39d3bc437..c94022c2bd 100644 --- a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java @@ -316,6 +316,23 @@ public interface HashBaseCommands { */ CompletableFuture hstrlen(String key, String field); + /** + * Returns the string length of the value associated with field in the hash stored at + * key. + * + * @see valkey.io for details. + * @param key The key of the hash. + * @param field The field in the hash. + * @return The string length or 0 if field or key does not + * exist. + * @example + *
{@code
+     * Long strlen = client.hstrlen(gs("my_hash"), gs("my_field")).get();
+     * assert strlen >= 0L;
+     * }
+ */ + CompletableFuture hstrlen(GlideString key, GlideString field); + /** * Returns a random field name from the hash value stored at key. * diff --git a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java index 427a10e336..4364e4f2ac 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.commands.LInsertOptions.InsertPosition; import glide.api.models.commands.LPosOptions; import glide.api.models.commands.ListDirection; @@ -271,6 +272,22 @@ CompletableFuture lposCount( */ CompletableFuture llen(String key); + /** + * Returns the length of the list stored at key. + * + * @see redis.io for details. + * @param key The key of the list. + * @return The length of the list at key.
+ * If key does not exist, it is interpreted as an empty list and 0 + * is returned. + * @example + *
{@code
+     * Long lenList = client.llen(gs("my_list")).get();
+     * assert lenList == 3L //Indicates that there are 3 elements in the list.;
+     * }
+ */ + CompletableFuture llen(GlideString key); + /** * Removes the first count occurrences of elements equal to element from * the list stored at key.
diff --git a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java index 3a2b200939..6542d9e3e8 100644 --- a/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StreamBaseCommands.java @@ -152,6 +152,21 @@ CompletableFuture>> xread( */ CompletableFuture xlen(String key); + /** + * Returns the number of entries in the stream stored at key. + * + * @see valkey.io for details. + * @param key The key of the stream. + * @return The number of entries in the stream. If key does not exist, return 0 + * . + * @example + *
{@code
+     * Long num = client.xlen(gs("key")).get();
+     * assert num == 2L; // Stream has 2 entries
+     * }
+ */ + CompletableFuture xlen(GlideString key); + /** * Removes the specified entries by id from a stream, and returns the number of entries deleted. * diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java index 3c7b1d899b..ea90f30df4 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -404,6 +404,26 @@ public interface StringBaseCommands { */ CompletableFuture strlen(String key); + /** + * Returns the length of the string value stored at key. + * + * @see redis.io for details. + * @param key The key to check its length. + * @return The length of the string value stored at key.
+ * If key does not exist, it is treated as an empty string, and the command + * returns 0. + * @example + *
{@code
+     * client.set(gs("key"), gs("GLIDE")).get();
+     * Long len = client.strlen(gs("key")).get();
+     * assert len == 5L;
+     *
+     * len = client.strlen(gs("non_existing_key")).get();
+     * assert len == 0L;
+     * }
+ */ + CompletableFuture strlen(GlideString key); + /** * Overwrites part of the string stored at key, starting at the specified * offset, for the entire length of value.
From d6595bfc9cb1dd14b45a7d6d3bf486fcc4162144 Mon Sep 17 00:00:00 2001 From: Alon Arenberg <93711356+alon-arenberg@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:02:13 +0300 Subject: [PATCH 10/17] support zincrby, zrem, zrank, zscore and zcard with GlideString (#1633) --- .../src/main/java/glide/api/BaseClient.java | 32 +++++ .../api/commands/SortedSetBaseCommands.java | 104 +++++++++++++++ .../test/java/glide/api/RedisClientTest.java | 125 ++++++++++++++++++ 3 files changed, 261 insertions(+) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index f9303df43d..895e669794 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -1206,11 +1206,23 @@ public CompletableFuture zrem(@NonNull String key, @NonNull String[] membe return commandManager.submitNewCommand(ZRem, arguments, this::handleLongResponse); } + @Override + public CompletableFuture zrem(@NonNull GlideString key, @NonNull GlideString[] members) { + GlideString[] arguments = ArrayUtils.addFirst(members, key); + return commandManager.submitNewCommand(ZRem, arguments, this::handleLongResponse); + } + @Override public CompletableFuture zcard(@NonNull String key) { return commandManager.submitNewCommand(ZCard, new String[] {key}, this::handleLongResponse); } + @Override + public CompletableFuture zcard(@NonNull GlideString key) { + return commandManager.submitNewCommand( + ZCard, new GlideString[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture> zpopmin(@NonNull String key, long count) { return commandManager.submitNewCommand( @@ -1251,12 +1263,24 @@ public CompletableFuture zscore(@NonNull String key, @NonNull String mem ZScore, new String[] {key, member}, this::handleDoubleOrNullResponse); } + @Override + public CompletableFuture zscore(@NonNull GlideString key, @NonNull GlideString member) { + return commandManager.submitNewCommand( + ZScore, new GlideString[] {key, member}, this::handleDoubleOrNullResponse); + } + @Override public CompletableFuture zrank(@NonNull String key, @NonNull String member) { return commandManager.submitNewCommand( ZRank, new String[] {key, member}, this::handleLongOrNullResponse); } + @Override + public CompletableFuture zrank(@NonNull GlideString key, @NonNull GlideString member) { + return commandManager.submitNewCommand( + ZRank, new GlideString[] {key, member}, this::handleLongOrNullResponse); + } + @Override public CompletableFuture zrankWithScore(@NonNull String key, @NonNull String member) { return commandManager.submitNewCommand( @@ -1479,6 +1503,14 @@ public CompletableFuture zincrby( return commandManager.submitNewCommand(ZIncrBy, arguments, this::handleDoubleResponse); } + @Override + public CompletableFuture zincrby( + @NonNull GlideString key, double increment, @NonNull GlideString member) { + GlideString[] arguments = + new GlideString[] {key, gs(Double.toString(increment).getBytes()), member}; + return commandManager.submitNewCommand(ZIncrBy, arguments, this::handleDoubleResponse); + } + @Override public CompletableFuture zintercard(@NonNull String[] keys) { String[] arguments = ArrayUtils.addFirst(keys, Integer.toString(keys.length)); diff --git a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java index 12956d9425..473c48ffd2 100644 --- a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.GlideString; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; import glide.api.models.commands.RangeOptions.LexBoundary; @@ -198,6 +199,28 @@ CompletableFuture zaddIncr( */ CompletableFuture zrem(String key, String[] members); + /** + * Removes the specified members from the sorted set stored at key.
+ * Specified members that are not a member of this set are ignored. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param members An array of members to remove from the sorted set. + * @return The number of members that were removed from the sorted set, not including non-existing + * members.
+ * If key does not exist, it is treated as an empty sorted set, and this command + * returns 0. + * @example + *
{@code
+     * Long num1 = client.zrem(gs("mySortedSet"), new GlideString[] {gs("member1"), gs("member2")}).get();
+     * assert num1 == 2L; // Indicates that two members have been removed from the sorted set "mySortedSet".
+     *
+     * Long num2 = client.zrem(gs("nonExistingSortedSet"), new GlideString[] {gs("member1"), gs("member2")}).get();
+     * assert num2 == 0L; // Indicates that no members were removed as the sorted set "nonExistingSortedSet" does not exist.
+     * }
+ */ + CompletableFuture zrem(GlideString key, GlideString[] members); + /** * Returns the cardinality (number of elements) of the sorted set stored at key. * @@ -217,6 +240,25 @@ CompletableFuture zaddIncr( */ CompletableFuture zcard(String key); + /** + * Returns the cardinality (number of elements) of the sorted set stored at key. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @return The number of elements in the sorted set.
+ * If key does not exist, it is treated as an empty sorted set, and this command + * return 0. + * @example + *
{@code
+     * Long num1 = client.zcard(gs("mySortedSet")).get();
+     * assert num1 == 3L; // Indicates that there are 3 elements in the sorted set "mySortedSet".
+     *
+     * Long num2 = client.zcard((gs("nonExistingSortedSet")).get();
+     * assert num2 == 0L;
+     * }
+ */ + CompletableFuture zcard(GlideString key); + /** * Removes and returns up to count members with the lowest scores from the sorted set * stored at the specified key. @@ -373,6 +415,26 @@ CompletableFuture zaddIncr( */ CompletableFuture zscore(String key, String member); + /** + * Returns the score of member in the sorted set stored at key. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param member The member whose score is to be retrieved. + * @return The score of the member.
+ * If member does not exist in the sorted set, null is returned.
+ * If key does not exist, null is returned. + * @example + *
{@code
+     * Double num1 = client.zscore(gs("mySortedSet")), gs("member")).get();
+     * assert num1 == 10.5; // Indicates that the score of "member" in the sorted set "mySortedSet" is 10.5.
+     *
+     * Double num2 = client.zscore(gs("mySortedSet"), gs("nonExistingMember")).get();
+     * assert num2 == null;
+     * }
+ */ + CompletableFuture zscore(GlideString key, GlideString member); + /** * Returns the specified range of elements in the sorted set stored at key.
* ZRANGE can perform different types of range queries: by index (rank), by the @@ -581,6 +643,28 @@ CompletableFuture zrangestore( */ CompletableFuture zrank(String key, String member); + /** + * Returns the rank of member in the sorted set stored at key, with + * scores ordered from low to high, starting from 0.
+ * To get the rank of member with its score, see {@link #zrankWithScore}. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param member The member whose rank is to be retrieved. + * @return The rank of member in the sorted set.
+ * If key doesn't exist, or if member is not present in the set, + * null will be returned. + * @example + *
{@code
+     * Long num1 = client.zrank(gs("mySortedSet"), gs("member2")).get();
+     * assert num1 == 3L; // Indicates that "member2" has the second-lowest score in the sorted set "mySortedSet".
+     *
+     * Long num2 = client.zcard(gs("mySortedSet"), gs("nonExistingMember")).get();
+     * assert num2 == null; // Indicates that "nonExistingMember" is not present in the sorted set "mySortedSet".
+     * }
+ */ + CompletableFuture zrank(GlideString key, GlideString member); + /** * Returns the rank of member in the sorted set stored at key with its * score, where scores are ordered from the lowest to highest, starting from 0.
@@ -1335,6 +1419,26 @@ CompletableFuture> zinterWithScores( */ CompletableFuture zincrby(String key, double increment, String member); + /** + * Increments the score of member in the sorted set stored at key by + * increment.
+ * If member does not exist in the sorted set, it is added with increment + * as its score. If key does not exist, a new sorted set with the specified + * member as its sole member is created. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param increment The score increment. + * @param member A member of the sorted set. + * @return The new score of member. + * @example + *
{@code
+     * Double score = client.zincrby(gs("mySortedSet"), -3.14, gs("value")).get();
+     * assert score > 0; // member "value" existed in the set before score was altered
+     * }
+ */ + CompletableFuture zincrby(GlideString key, double increment, GlideString member); + /** * Returns the cardinality of the intersection of the sorted sets specified by keys. * diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 29a1119943..c5b0b97bab 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -2922,6 +2922,31 @@ public void zrem_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zrem_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] members = new GlideString[] {gs("member1"), gs("member2")}; + GlideString[] arguments = ArrayUtils.addFirst(members, key); + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRem), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrem(key, members); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zcard_returns_success() { @@ -2946,6 +2971,30 @@ public void zcard_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zcard_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] arguments = new GlideString[] {key}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zcard(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zpopmin_returns_success() { @@ -3119,6 +3168,31 @@ public void zscore_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zscore_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member = gs("testMember"); + GlideString[] arguments = new GlideString[] {key, member}; + Double value = 3.5; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZScore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zscore(key, member); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zrange_by_index_returns_success() { @@ -3288,6 +3362,31 @@ public void zrank_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zrank_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString member = gs("testMember"); + GlideString[] arguments = new GlideString[] {key, member}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zrank(key, member); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zrankWithScore_returns_success() { @@ -4101,6 +4200,32 @@ public void zincrby_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zincrby_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + double increment = 4.2; + GlideString member = gs("member"); + GlideString[] arguments = new GlideString[] {key, gs("4.2"), member}; + Double value = 3.14; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZIncrBy), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zincrby(key, increment, member); + Double payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void xadd_returns_success() { From 66d97e57627efcbaf4b9e70001e8c39e34d0fb5e Mon Sep 17 00:00:00 2001 From: Alon Arenberg <93711356+alon-arenberg@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:58:02 +0300 Subject: [PATCH 11/17] =?UTF-8?q?support=20zintercard,=20zdiffstore,=20zre?= =?UTF-8?q?mrangebyrank,=20lpush,=20lpushx,=20lrem,=E2=80=A6=20(#1634)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit support zintercard, zdiffstore, zremrangebyrank, lpush, lpushx, lrem, rpus and rpushx with GlideString --- .../src/main/java/glide/api/BaseClient.java | 69 ++++++ .../glide/api/commands/ListBaseCommands.java | 100 ++++++++ .../api/commands/SortedSetBaseCommands.java | 84 +++++++ .../test/java/glide/api/RedisClientTest.java | 232 ++++++++++++++++++ 4 files changed, 485 insertions(+) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 895e669794..f45e23ea0e 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -863,6 +863,12 @@ public CompletableFuture lpush(@NonNull String key, @NonNull String[] elem return commandManager.submitNewCommand(LPush, arguments, this::handleLongResponse); } + @Override + public CompletableFuture lpush(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(LPush, arguments, this::handleLongResponse); + } + @Override public CompletableFuture lpop(@NonNull String key) { return commandManager.submitNewCommand( @@ -948,12 +954,27 @@ public CompletableFuture lrem(@NonNull String key, long count, @NonNull St LRem, new String[] {key, Long.toString(count), element}, this::handleLongResponse); } + @Override + public CompletableFuture lrem( + @NonNull GlideString key, long count, @NonNull GlideString element) { + return commandManager.submitNewCommand( + LRem, + new GlideString[] {key, gs(Long.toString(count).getBytes()), element}, + this::handleLongResponse); + } + @Override public CompletableFuture rpush(@NonNull String key, @NonNull String[] elements) { String[] arguments = ArrayUtils.addFirst(elements, key); return commandManager.submitNewCommand(RPush, arguments, this::handleLongResponse); } + @Override + public CompletableFuture rpush(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(RPush, arguments, this::handleLongResponse); + } + @Override public CompletableFuture rpop(@NonNull String key) { return commandManager.submitNewCommand( @@ -1332,6 +1353,15 @@ public CompletableFuture zdiffstore(@NonNull String destination, @NonNull return commandManager.submitNewCommand(ZDiffStore, arguments, this::handleLongResponse); } + @Override + public CompletableFuture zdiffstore( + @NonNull GlideString destination, @NonNull GlideString[] keys) { + GlideString[] arguments = + ArrayUtils.addAll( + new GlideString[] {destination, gs(Long.toString(keys.length).getBytes())}, keys); + return commandManager.submitNewCommand(ZDiffStore, arguments, this::handleLongResponse); + } + @Override public CompletableFuture zcount( @NonNull String key, @NonNull ScoreRange minScore, @NonNull ScoreRange maxScore) { @@ -1347,6 +1377,16 @@ public CompletableFuture zremrangebyrank(@NonNull String key, long start, this::handleLongResponse); } + @Override + public CompletableFuture zremrangebyrank(@NonNull GlideString key, long start, long end) { + return commandManager.submitNewCommand( + ZRemRangeByRank, + new GlideString[] { + key, gs(Long.toString(start).getBytes()), gs(Long.toString(end).getBytes()) + }, + this::handleLongResponse); + } + @Override public CompletableFuture zremrangebylex( @NonNull String key, @NonNull LexRange minLex, @NonNull LexRange maxLex) { @@ -1517,6 +1557,13 @@ public CompletableFuture zintercard(@NonNull String[] keys) { return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); } + @Override + public CompletableFuture zintercard(@NonNull GlideString[] keys) { + GlideString[] arguments = + ArrayUtils.addFirst(keys, gs(Integer.toString(keys.length).getBytes())); + return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); + } + @Override public CompletableFuture zintercard(@NonNull String[] keys, long limit) { String[] arguments = @@ -1527,6 +1574,16 @@ public CompletableFuture zintercard(@NonNull String[] keys, long limit) { return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); } + @Override + public CompletableFuture zintercard(@NonNull GlideString[] keys, long limit) { + GlideString[] arguments = + concatenateArrays( + new GlideString[] {gs(Integer.toString(keys.length).getBytes())}, + keys, + new GlideString[] {gs(LIMIT_REDIS_API), gs(Long.toString(limit).getBytes())}); + return commandManager.submitNewCommand(ZInterCard, arguments, this::handleLongResponse); + } + @Override public CompletableFuture xadd(@NonNull String key, @NonNull Map values) { return xadd(key, values, StreamAddOptions.builder().build()); @@ -1724,12 +1781,24 @@ public CompletableFuture rpushx(@NonNull String key, @NonNull String[] ele return commandManager.submitNewCommand(RPushX, arguments, this::handleLongResponse); } + @Override + public CompletableFuture rpushx(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(RPushX, arguments, this::handleLongResponse); + } + @Override public CompletableFuture lpushx(@NonNull String key, @NonNull String[] elements) { String[] arguments = ArrayUtils.addFirst(elements, key); return commandManager.submitNewCommand(LPushX, arguments, this::handleLongResponse); } + @Override + public CompletableFuture lpushx(@NonNull GlideString key, @NonNull GlideString[] elements) { + GlideString[] arguments = ArrayUtils.addFirst(elements, key); + return commandManager.submitNewCommand(LPushX, arguments, this::handleLongResponse); + } + @Override public CompletableFuture zrange( @NonNull String key, @NonNull RangeQuery rangeQuery, boolean reverse) { diff --git a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java index 4364e4f2ac..53dfba94db 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -40,6 +40,27 @@ public interface ListBaseCommands { */ CompletableFuture lpush(String key, String[] elements); + /** + * Inserts all the specified values at the head of the list stored at key. + * elements are inserted one after the other to the head of the list, from the leftmost + * element to the rightmost element. If key does not exist, it is created as an empty + * list before performing the push operation. + * + * @see redis.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the head of the list stored at key. + * @return The length of the list after the push operation. + * @example + *
{@code
+     * Long pushCount1 = client.lpush(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert pushCount1 == 2L;
+     *
+     * Long pushCount2 = client.lpush(gs("nonexistent_list"), new GlideString[] {gs("new_value")}).get();
+     * assert pushCount2 == 1L;
+     * }
+ */ + CompletableFuture lpush(GlideString key, GlideString[] elements); + /** * Removes and returns the first elements of the list stored at key. The command pops * a single element from the beginning of the list. @@ -312,6 +333,30 @@ CompletableFuture lposCount( */ CompletableFuture lrem(String key, long count, String element); + /** + * Removes the first count occurrences of elements equal to element from + * the list stored at key.
+ * If count is positive: Removes elements equal to element moving from + * head to tail.
+ * If count is negative: Removes elements equal to element moving from + * tail to head.
+ * If count is 0 or count is greater than the occurrences of elements + * equal to element, it removes all elements equal to element. + * + * @see redis.io for details. + * @param key The key of the list. + * @param count The count of the occurrences of elements equal to element to remove. + * @param element The element to remove from the list. + * @return The number of the removed elements.
+ * If key does not exist, 0 is returned. + * @example + *
{@code
+     * Long num = client.rem(gs("my_list"), 2, gs("value")).get();
+     * assert num == 2L;
+     * }
+ */ + CompletableFuture lrem(GlideString key, long count, GlideString element); + /** * Inserts all the specified values at the tail of the list stored at key.
* elements are inserted one after the other to the tail of the list, from the @@ -333,6 +378,27 @@ CompletableFuture lposCount( */ CompletableFuture rpush(String key, String[] elements); + /** + * Inserts all the specified values at the tail of the list stored at key.
+ * elements are inserted one after the other to the tail of the list, from the + * leftmost element to the rightmost element. If key does not exist, it is created as + * an empty list before performing the push operation. + * + * @see redis.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the tail of the list stored at key. + * @return The length of the list after the push operation. + * @example + *
{@code
+     * Long pushCount1 = client.rpush(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert pushCount1 == 2L;
+     *
+     * Long pushCount2 = client.rpush(gs("nonexistent_list"), new GlideString[] {gs("new_value")}).get();
+     * assert pushCount2 == 1L;
+     * }
+ */ + CompletableFuture rpush(GlideString key, GlideString[] elements); + /** * Removes and returns the last elements of the list stored at key.
* The command pops a single element from the end of the list. @@ -471,6 +537,23 @@ CompletableFuture linsert( */ CompletableFuture rpushx(String key, String[] elements); + /** + * Inserts all the specified values at the tail of the list stored at key, only if + * key exists and holds a list. If key is not a list, this performs no + * operation. + * + * @see redis.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the tail of the list stored at key. + * @return The length of the list after the push operation. + * @example + *
{@code
+     * Long listLength = client.rpushx(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert listLength >= 2L;
+     * }
+ */ + CompletableFuture rpushx(GlideString key, GlideString[] elements); + /** * Inserts all the specified values at the head of the list stored at key, only if * key exists and holds a list. If key is not a list, this performs no @@ -488,6 +571,23 @@ CompletableFuture linsert( */ CompletableFuture lpushx(String key, String[] elements); + /** + * Inserts all the specified values at the head of the list stored at key, only if + * key exists and holds a list. If key is not a list, this performs no + * operation. + * + * @see redis.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the head of the list stored at key. + * @return The length of the list after the push operation. + * @example + *
{@code
+     * Long listLength = client.lpushx(gs("my_list"), new GlideString[] {gs("value1"), gs("value2")}).get();
+     * assert listLength >= 2L;
+     * }
+ */ + CompletableFuture lpushx(GlideString key, GlideString[] elements); + /** * Pops one or more elements from the first non-empty list from the provided keys * . diff --git a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java index 473c48ffd2..fe1cf1f181 100644 --- a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java @@ -810,6 +810,26 @@ CompletableFuture zrangestore( */ CompletableFuture zdiffstore(String destination, String[] keys); + /** + * Calculates the difference between the first sorted set and all the successive sorted sets at + * keys and stores the difference as a sorted set to destination, + * overwriting it if it already exists. Non-existent keys are treated as empty sets. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @since Redis 6.2 and above. + * @see redis.io for more details. + * @param destination The key for the resulting sorted set. + * @param keys The keys of the sorted sets to compare. + * @return The number of members in the resulting sorted set stored at destination. + * @example + *
{@code
+     * Long payload = client.zdiffstore(gs("mySortedSet"), new GlideString[] {gs("key1"), gs("key2")}).get();
+     * assert payload > 0; // At least one member differed in "key1" compared to "key2", and this difference was stored in "mySortedSet".
+     * }
+ */ + CompletableFuture zdiffstore(GlideString destination, GlideString[] keys); + /** * Returns the number of members in the sorted set stored at key with scores between * minScore and maxScore. @@ -864,6 +884,33 @@ CompletableFuture zrangestore( */ CompletableFuture zremrangebyrank(String key, long start, long end); + /** + * Removes all elements in the sorted set stored at key with rank between start + * and end. Both start and end are zero-based + * indexes with 0 being the element with the lowest score. These indexes can be + * negative numbers, where they indicate offsets starting at the element with the highest score. + * + * @see redis.io for more details. + * @param key The key of the sorted set. + * @param start The starting point of the range. + * @param end The end of the range. + * @return The number of elements removed.
+ * If start exceeds the end of the sorted set, or if start is + * greater than end, 0 returned.
+ * If end exceeds the actual end of the sorted set, the range will stop at the + * actual end of the sorted set.
+ * If key does not exist 0 will be returned. + * @example + *
{@code
+     * Long payload1 = client.zremrangebyrank(gs("mySortedSet"), 0, 4).get();
+     * assert payload1 == 5L; // Indicates that 5 elements, with ranks ranging from 0 to 4 (inclusive), have been removed from "mySortedSet".
+     *
+     * Long payload2 = client.zremrangebyrank(gs("mySortedSet"), 0, 4).get();
+     * assert payload2 == 0L; // Indicates that nothing was removed.
+     * }
+ */ + CompletableFuture zremrangebyrank(GlideString key, long start, long end); + /** * Removes all elements in the sorted set stored at key with a lexicographical order * between minLex and maxLex. @@ -1455,6 +1502,22 @@ CompletableFuture> zinterWithScores( */ CompletableFuture zintercard(String[] keys); + /** + * Returns the cardinality of the intersection of the sorted sets specified by keys. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Redis 7.0 and above. + * @see redis.io for more details. + * @param keys The keys of the sorted sets to intersect. + * @return The cardinality of the intersection of the given sorted sets. + * @example + *
{@code
+     * Long length = client.zintercard(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")}).get();
+     * assert length == 3L;
+     * }
+ */ + CompletableFuture zintercard(GlideString[] keys); + /** * Returns the cardinality of the intersection of the sorted sets specified by keys. * If the intersection cardinality reaches limit partway through the computation, the @@ -1475,4 +1538,25 @@ CompletableFuture> zinterWithScores( * }
*/ CompletableFuture zintercard(String[] keys, long limit); + + /** + * Returns the cardinality of the intersection of the sorted sets specified by keys. + * If the intersection cardinality reaches limit partway through the computation, the + * algorithm will exit early and yield limit as the cardinality. + * + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @since Redis 7.0 and above. + * @see redis.io for more details. + * @param keys The keys of the sorted sets to intersect. + * @param limit Specifies a maximum number for the intersection cardinality. If limit is set to + * 0 the range will be unlimited. + * @return The cardinality of the intersection of the given sorted sets, or the limit + * if reached. + * @example + *
{@code
+     * Long length = client.zintercard(new GlideString[] {gs("mySortedSet1"), gs("mySortedSet2")}, 5).get();
+     * assert length == 3L;
+     * }
+ */ + CompletableFuture zintercard(GlideString[] keys, long limit); } diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index c5b0b97bab..a578b30d21 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -1900,6 +1900,31 @@ public void lpush_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lpush_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpush(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void lpop_returns_success() { @@ -2165,6 +2190,31 @@ public void lrem_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lrem_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long count = 2L; + GlideString element = gs("value"); + GlideString[] args = new GlideString[] {key, gs(Long.toString(count).getBytes()), element}; + long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LRem), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lrem(key, count, element); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void rpush_returns_success() { @@ -2190,6 +2240,31 @@ public void rpush_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void rpush_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPush), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpush(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void rpop_returns_success() { @@ -3512,6 +3587,34 @@ public void zdiffstore_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zdiffstore_binary_returns_success() { + // setup + GlideString destKey = gs("testDestKey"); + GlideString[] keys = new GlideString[] {gs("testKey1"), gs("testKey2")}; + GlideString[] arguments = + new GlideString[] { + destKey, gs(Long.toString(keys.length).getBytes()), gs("testKey1"), gs("testKey2") + }; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZDiffStore), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zdiffstore(destKey, keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zdiff_returns_success() { @@ -3614,6 +3717,35 @@ public void zremrangebyrank_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zremrangebyrank_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + long start = 0; + long end = -1; + GlideString[] arguments = + new GlideString[] { + key, gs(Long.toString(start).getBytes()), gs(Long.toString(end).getBytes()) + }; + Long value = 5L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZRemRangeByRank), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zremrangebyrank(key, start, end); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zremrangebylex_returns_success() { @@ -4076,6 +4208,32 @@ public void zintercard_with_limit_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zintercard_with_limit_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + long limit = 3L; + GlideString[] arguments = + new GlideString[] {gs("2"), gs("key1"), gs("key2"), gs(LIMIT_REDIS_API), gs("3")}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zintercard(keys, limit); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zintercard_returns_success() { @@ -4100,6 +4258,30 @@ public void zintercard_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zintercard_binary_returns_success() { + // setup + GlideString[] keys = new GlideString[] {gs("key1"), gs("key2")}; + GlideString[] arguments = new GlideString[] {gs("2"), gs("key1"), gs("key2")}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ZInterCard), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.zintercard(keys); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void zrandmember_returns_success() { @@ -5357,6 +5539,31 @@ public void rpushx_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void rpushx_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(RPushX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.rpushx(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void lpushx_returns_success() { @@ -5382,6 +5589,31 @@ public void lpushx_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lpushx_binary_returns_success() { + // setup + GlideString key = gs("testKey"); + GlideString[] elements = new GlideString[] {gs("value1"), gs("value2")}; + GlideString[] args = new GlideString[] {key, gs("value1"), gs("value2")}; + Long value = 2L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LPushX), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lpushx(key, elements); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void brpop_returns_success() { From a8e7f77e7e60be0587cad485f77d54750399e0cd Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:49:56 -0700 Subject: [PATCH 12/17] Python: add XREAD command (#1644) * Python: add XREAD command * Minor doc fix * PR suggestions --- CHANGELOG.md | 1 + python/python/glide/__init__.py | 2 + python/python/glide/async_commands/core.py | 53 +++++- python/python/glide/async_commands/stream.py | 33 ++++ .../glide/async_commands/transaction.py | 34 +++- python/python/tests/test_async_client.py | 177 ++++++++++++++++++ python/python/tests/test_transaction.py | 2 + 7 files changed, 298 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1d018d210..87f5b5a2d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ * 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)) * Python: Added XREVRANGE command ([#1625](https://github.com/aws/glide-for-redis/pull/1625)) +* Python: Added XREAD command ([#1644](https://github.com/aws/glide-for-redis/pull/1644)) ### 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 c8e8b0a60b..dc8881a8e6 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -53,6 +53,7 @@ MinId, StreamAddOptions, StreamRangeBound, + StreamReadOptions, StreamTrimOptions, TrimByMaxLen, TrimByMinId, @@ -159,6 +160,7 @@ "MinId", "StreamAddOptions", "StreamRangeBound", + "StreamReadOptions", "StreamTrimOptions", "TrimByMaxLen", "TrimByMinId", diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index c131127d6e..f509d647bc 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -48,6 +48,7 @@ from glide.async_commands.stream import ( StreamAddOptions, StreamRangeBound, + StreamReadOptions, StreamTrimOptions, ) from glide.constants import TOK, TResult @@ -2683,7 +2684,7 @@ async def xrange( Returns: Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a - list of pairings with format `[[field, entry], [field, entry], ...]`. Returns null if the range + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range arguments are not applicable. Examples: @@ -2732,7 +2733,7 @@ async def xrevrange( Returns: Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a - list of pairings with format `[[field, entry], [field, entry], ...]`. Returns null if the range + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range arguments are not applicable. Examples: @@ -2753,6 +2754,54 @@ async def xrevrange( await self._execute_command(RequestType.XRevRange, args), ) + async def xread( + self, + keys_and_ids: Mapping[str, str], + options: Optional[StreamReadOptions] = None, + ) -> Optional[Mapping[str, Mapping[str, List[List[str]]]]]: + """ + Reads entries from the given streams. + + See https://valkey.io/commands/xread for more details. + + Note: + When in cluster mode, all keys in `keys_and_ids` must map to the same hash slot. + + Args: + keys_and_ids (Mapping[str, str]): A mapping of keys and entry IDs to read from. The mapping is composed of a + stream's key and the ID of the entry after which the stream will be read. + options (Optional[StreamReadOptions]): Options detailing how to read the stream. + + Returns: + Optional[Mapping[str, Mapping[str, List[List[str]]]]]: A mapping of stream keys, to a mapping of stream IDs, + to a list of pairings with format `[[field, entry], [field, entry], ...]`. + None will be returned under the following conditions: + - All key-ID pairs in `keys_and_ids` have either a non-existing key or a non-existing ID, or there are no entries after the given ID. + - The `BLOCK` option is specified and the timeout is hit. + + Examples: + >>> await client.xadd("mystream", [("field1", "value1")], StreamAddOptions(id="0-1")) + >>> await client.xadd("mystream", [("field2", "value2"), ("field2", "value3")], StreamAddOptions(id="0-2")) + >>> await client.xread({"mystream": "0-0"}, StreamReadOptions(block_ms=1000)) + { + "mystream": { + "0-1": [["field1", "value1"]], + "0-2": [["field2", "value2"], ["field2", "value3"]], + } + } + # Indicates the stream entries for "my_stream" with IDs greater than "0-0". The operation blocks up to + # 1000ms if there is no stream data. + """ + args = [] if options is None else options.to_args() + args.append("STREAMS") + args.extend([key for key in keys_and_ids.keys()]) + args.extend([value for value in keys_and_ids.values()]) + + return cast( + Optional[Mapping[str, Mapping[str, List[List[str]]]]], + await self._execute_command(RequestType.XRead, args), + ) + async def geoadd( self, key: str, diff --git a/python/python/glide/async_commands/stream.py b/python/python/glide/async_commands/stream.py index 114179f2c6..6d301d820a 100644 --- a/python/python/glide/async_commands/stream.py +++ b/python/python/glide/async_commands/stream.py @@ -232,3 +232,36 @@ def __init__(self, stream_id: str): def to_arg(self) -> str: return self.stream_id + + +class StreamReadOptions: + READ_COUNT_REDIS_API = "COUNT" + READ_BLOCK_REDIS_API = "BLOCK" + + def __init__(self, block_ms: Optional[int] = None, count: Optional[int] = None): + """ + Options for reading entries from streams. Can be used as an optional argument to `XREAD`. + + Args: + block_ms (Optional[int]): If provided, the request will be blocked for the set amount of milliseconds or + until the server has the required number of entries. Equivalent to `BLOCK` in the Redis API. + count (Optional[int]): The maximum number of elements requested. Equivalent to `COUNT` in the Redis API. + """ + self.block_ms = block_ms + self.count = count + + def to_args(self) -> List[str]: + """ + Returns the options as a list of string arguments to be used in the `XREAD` command. + + Returns: + List[str]: The options as a list of arguments for the `XREAD` command. + """ + args = [] + if self.block_ms is not None: + args.extend([self.READ_BLOCK_REDIS_API, str(self.block_ms)]) + + if self.count is not None: + args.extend([self.READ_COUNT_REDIS_API, str(self.count)]) + + return args diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index ba8e685f37..b995f753a3 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -45,6 +45,7 @@ from glide.async_commands.stream import ( StreamAddOptions, StreamRangeBound, + StreamReadOptions, StreamTrimOptions, ) from glide.protobuf.redis_request_pb2 import RequestType @@ -1879,7 +1880,7 @@ def xrange( Command response: Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a - list of pairings with format `[[field, entry], [field, entry], ...]`. Returns null if the range arguments + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range arguments are not applicable. """ args = [key, start.to_arg(), end.to_arg()] @@ -1916,7 +1917,7 @@ def xrevrange( Command response: Optional[Mapping[str, List[List[str]]]]: A mapping of stream IDs to stream entry data, where entry data is a - list of pairings with format `[[field, entry], [field, entry], ...]`. Returns null if the range arguments + list of pairings with format `[[field, entry], [field, entry], ...]`. Returns None if the range arguments are not applicable. """ args = [key, end.to_arg(), start.to_arg()] @@ -1925,6 +1926,35 @@ def xrevrange( return self.append_command(RequestType.XRevRange, args) + def xread( + self: TTransaction, + keys_and_ids: Mapping[str, str], + options: Optional[StreamReadOptions] = None, + ) -> TTransaction: + """ + Reads entries from the given streams. + + See https://valkey.io/commands/xread for more details. + + Args: + keys_and_ids (Mapping[str, str]): A mapping of keys and entry IDs to read from. The mapping is composed of a + stream's key and the ID of the entry after which the stream will be read. + options (Optional[StreamReadOptions]): Options detailing how to read the stream. + + Command response: + Optional[Mapping[str, Mapping[str, List[List[str]]]]]: A mapping of stream keys, to a mapping of stream IDs, + to a list of pairings with format `[[field, entry], [field, entry], ...]`. + None will be returned under the following conditions: + - All key-ID pairs in `keys_and_ids` have either a non-existing key or a non-existing ID, or there are no entries after the given ID. + - The `BLOCK` option is specified and the timeout is hit. + """ + args = [] if options is None else options.to_args() + args.append("STREAMS") + args.extend([key for key in keys_and_ids.keys()]) + args.extend([value for value in keys_and_ids.values()]) + + return self.append_command(RequestType.XRead, args) + def geoadd( self: TTransaction, key: str, diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 20be6a28f6..24407e4592 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -61,6 +61,7 @@ MaxId, MinId, StreamAddOptions, + StreamReadOptions, TrimByMaxLen, TrimByMinId, ) @@ -4896,6 +4897,181 @@ async def test_xrange_and_xrevrange(self, redis_client: TRedisClient): with pytest.raises(RequestError): await redis_client.xrevrange(key, IdBound("not_a_stream_id"), MinId()) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xread( + self, redis_client: TRedisClient, cluster_mode, protocol, request + ): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:3-{get_random_string(10)}" + stream_id1_1 = "1-1" + stream_id1_2 = "1-2" + stream_id1_3 = "1-3" + stream_id2_1 = "2-1" + stream_id2_2 = "2-2" + stream_id2_3 = "2-3" + non_existing_id = "99-99" + + # setup first entries in streams key1 and key2 + assert ( + await redis_client.xadd( + key1, [("f1_1", "v1_1")], StreamAddOptions(id=stream_id1_1) + ) + == stream_id1_1 + ) + assert ( + await redis_client.xadd( + key2, [("f2_1", "v2_1")], StreamAddOptions(id=stream_id2_1) + ) + == stream_id2_1 + ) + + # setup second entries in streams key1 and key2 + assert ( + await redis_client.xadd( + key1, [("f1_2", "v1_2")], StreamAddOptions(id=stream_id1_2) + ) + == stream_id1_2 + ) + assert ( + await redis_client.xadd( + key2, [("f2_2", "v2_2")], StreamAddOptions(id=stream_id2_2) + ) + == stream_id2_2 + ) + + # setup third entries in streams key1 and key2 + assert ( + await redis_client.xadd( + key1, [("f1_3", "v1_3")], StreamAddOptions(id=stream_id1_3) + ) + == stream_id1_3 + ) + assert ( + await redis_client.xadd( + key2, [("f2_3", "v2_3")], StreamAddOptions(id=stream_id2_3) + ) + == stream_id2_3 + ) + + assert await redis_client.xread({key1: stream_id1_1, key2: stream_id2_1}) == { + key1: { + stream_id1_2: [["f1_2", "v1_2"]], + stream_id1_3: [["f1_3", "v1_3"]], + }, + key2: { + stream_id2_2: [["f2_2", "v2_2"]], + stream_id2_3: [["f2_3", "v2_3"]], + }, + } + + assert await redis_client.xread({non_existing_key: stream_id1_1}) is None + assert await redis_client.xread({key1: non_existing_id}) is None + + # passing an empty read options argument has no effect + assert await redis_client.xread({key1: stream_id1_1}, StreamReadOptions()) == { + key1: { + stream_id1_2: [["f1_2", "v1_2"]], + stream_id1_3: [["f1_3", "v1_3"]], + }, + } + + assert await redis_client.xread( + {key1: stream_id1_1}, StreamReadOptions(count=1) + ) == { + key1: { + stream_id1_2: [["f1_2", "v1_2"]], + }, + } + assert await redis_client.xread( + {key1: stream_id1_1}, StreamReadOptions(count=1, block_ms=1000) + ) == { + key1: { + stream_id1_2: [["f1_2", "v1_2"]], + }, + } + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_xread_edge_cases_and_failures( + self, redis_client: TRedisClient, cluster_mode, protocol, request + ): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + string_key = f"{{testKey}}:2-{get_random_string(10)}" + stream_id0 = "0-0" + stream_id1 = "1-1" + stream_id2 = "1-2" + + assert ( + await redis_client.xadd( + key1, [("f1", "v1")], StreamAddOptions(id=stream_id1) + ) + == stream_id1 + ) + assert ( + await redis_client.xadd( + key1, [("f2", "v2")], StreamAddOptions(id=stream_id2) + ) + == stream_id2 + ) + + test_client = await create_client( + request=request, protocol=protocol, cluster_mode=cluster_mode, timeout=900 + ) + # ensure command doesn't time out even if timeout > request timeout + assert ( + await test_client.xread( + {key1: stream_id2}, StreamReadOptions(block_ms=1000) + ) + is None + ) + + async def endless_xread_call(): + await test_client.xread({key1: stream_id2}, StreamReadOptions(block_ms=0)) + + # when xread is called with a block timeout of 0, it should never timeout, but we wrap the test with a timeout + # to avoid the test getting stuck forever. + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_xread_call(), timeout=3) + + # if count is non-positive, it is ignored + assert await redis_client.xread( + {key1: stream_id0}, StreamReadOptions(count=0) + ) == { + key1: { + stream_id1: [["f1", "v1"]], + stream_id2: [["f2", "v2"]], + }, + } + assert await redis_client.xread( + {key1: stream_id0}, StreamReadOptions(count=-1) + ) == { + key1: { + stream_id1: [["f1", "v1"]], + stream_id2: [["f2", "v2"]], + }, + } + + # invalid stream ID + with pytest.raises(RequestError): + await redis_client.xread({key1: "invalid_stream_id"}) + + # invalid argument - block cannot be negative + with pytest.raises(RequestError): + await redis_client.xread({key1: stream_id1}, StreamReadOptions(block_ms=-1)) + + # invalid argument - keys_and_ids must not be empty + with pytest.raises(RequestError): + await redis_client.xread({}) + + # key exists, but it is not a stream + assert await redis_client.set(string_key, "foo") + with pytest.raises(RequestError): + await redis_client.xread({string_key: stream_id1, key1: stream_id1}) + with pytest.raises(RequestError): + await redis_client.xread({key1: stream_id1, string_key: stream_id1}) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_pfadd(self, redis_client: TRedisClient): @@ -5793,6 +5969,7 @@ async def test_multi_key_command_returns_cross_slot_error( redis_client.msetnx({"abc": "abc", "zxy": "zyx"}), redis_client.sunion(["def", "ghi"]), redis_client.bitop(BitwiseOperation.OR, "abc", ["zxy", "lkn"]), + redis_client.xread({"abc": "0-0", "zxy": "0-0"}), ] if not await check_if_server_version_lt(redis_client, "6.2.0"): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 9f5acb64bd..23f5be1d30 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -474,6 +474,8 @@ async def transaction_test( args.append("0-2") transaction.xlen(key11) args.append(2) + transaction.xread({key11: "0-1"}) + args.append({key11: {"0-2": [["foo", "bar"]]}}) transaction.xrange(key11, IdBound("0-1"), IdBound("0-1")) args.append({"0-1": [["foo", "bar"]]}) transaction.xrevrange(key11, IdBound("0-1"), IdBound("0-1")) From b2ece6c5fcf5c232e7a91bc725f91406148e9fd5 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Mon, 24 Jun 2024 16:57:55 -0700 Subject: [PATCH 13/17] implement readme --- java/README.md | 100 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/java/README.md b/java/README.md index 5d0077c3da..ca972b9276 100644 --- a/java/README.md +++ b/java/README.md @@ -1,18 +1,37 @@ +# GLIDE for Redis + +General Language Independent Driver for the Enterprise (GLIDE) for Redis, is an AWS-sponsored, open-source Redis client. GLIDE for Redis works with any Redis distribution that adheres to the Redis Serialization +Protocol (RESP) specification, including open-source Redis, Amazon ElastiCache for Redis, and Amazon MemoryDB for Redis. +Strategic, mission-critical Redis-based applications have requirements for security, optimized performance, minimal downtime, and observability. GLIDE for Redis is designed to provide a client experience that helps meet these objectives. +It is sponsored and supported by AWS, and comes pre-configured with best practices learned from over a decade of operating Redis-compatible services used by hundreds of thousands of customers. +To help ensure consistency in development and operations, GLIDE for Redis is implemented using a core driver framework, written in Rust, with extensions made available for each supported programming language. This design ensures that updates easily propagate to each language and reduces overall complexity. +In this release, GLIDE for Redis is available for Python, Javascript (Node.js), and Java. + +## Supported Redis Versions + +GLIDE for Redis is API-compatible with open source Redis version 6 and 7. + +## Current Status + +We've made GLIDE for Redis an open-source project, and are releasing it in Preview to the community to gather feedback, and actively collaborate on the project roadmap. We welcome questions and contributions from all Redis stakeholders. +This preview release is recommended for testing purposes only. + # Getting Started - Java Wrapper -## Notice: Java Wrapper - Work in Progress +## System Requirements + +The beta release of GLIDE for Redis was tested on Intel x86_64 using Ubuntu 22.04.1, Amazon Linux 2023 (AL2023), and macOS 12.7. -We're excited to share that the Java client is currently in development! However, it's important to note that this client -is a work in progress and is not yet complete or fully tested. Your contributions and feedback are highly encouraged as -we work towards refining and improving this implementation. Thank you for your interest and understanding as we continue -to develop this Java wrapper. +## Java supported version +JDK 11+. The Java client contains the following parts: -1. `client`: A Java-wrapper around the rust-core client. -2. `examples`: An examples app to test the client against a Redis localhost -3. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of GLIDE for Redis and other Java clients. -4. `integTest`: An integration test sub-project for API and E2E testing +1. `src`: Rust dynamic library FFI to integrate with [GLIDE core library](https://github.com/aws/glide-for-redis/blob/main/glide-core/README.md). +2. `client`: A Java-wrapper around the [GLIDE core rust library](../glide-core/README.md) and unit tests for it. +3. `examples`: An examples app to test the client against a Redis localhost. +4. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of GLIDE for Redis and other Java clients. +5. `integTest`: An integration test sub-project for API and E2E testing. ## Installation and Setup @@ -29,27 +48,49 @@ Software Dependencies: - protoc (protobuf compiler) - Rust +Please also consider installing the following packages to build [GLIDE core rust library](../glide-core/README.md): + +- GCC +- pkg-config +- openssl +- openssl-dev + #### Prerequisites +**Protoc installation** + +Download a binary matching your system from the [official release page](https://github.com/protocolbuffers/protobuf/releases) and make it accessible in your $PATH by moving it or creating a symlink. +For example, on Linux you can copy it to `/usr/bin`: + +```bash +sudo cp protoc /usr/bin/ +``` + **Dependencies installation for Ubuntu** + ```bash sudo apt update -y -sudo apt install -y protobuf-compiler openjdk-11-jdk openssl gcc +sudo apt install -y openjdk-11-jdk openssl gcc curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" ``` -**Dependencies for MacOS** +**Dependencies installation for MacOS** -Ensure that you have a minimum Java version of JDK 11 installed on your system: ```bash - $ echo $JAVA_HOME -/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home +brew update +brew install git gcc pkgconfig openssl openjdk@11 +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +``` + +**Java version check** -$ java -version - openjdk version "11.0.1" 2018-10-16 - OpenJDK Runtime Environment 18.9 (build 11.0.1+13) - OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode) +Ensure that you have a minimum Java version of JDK 11 installed on your system: + +```bash +echo $JAVA_HOME +java -version ``` #### Building and installation steps @@ -106,6 +147,29 @@ CompletableFuture getResponse = client.get("key"); assert getResponse.get() == "foobar" : "Failed on client.get("key") request"; ``` +### Cluster Redis: +```java +import glide.api.RedisClusterClient; + +String host = "localhost"; +Integer port = 6379; +boolean useSsl = false; + +RedisClientConfiguration config = + RedisClientConfiguration.builder() + .address(NodeAddress.builder().host(host).port(port).build()) + .useTLS(useSsl) + .build(); + +RedisClusterClient client = RedisClusterClient.CreateClient(config).get(); + +CompletableFuture setResponse = client.set("key", "foobar"); +assert setResponse.get() == "OK" : "Failed on client.set("key", "foobar") request"; + +CompletableFuture getResponse = client.get("key"); +assert getResponse.get() == "foobar" : "Failed on client.get("key") request"; +``` + ### Benchmarks You can run benchmarks using `./gradlew run`. You can set arguments using the args flag like: From 7ffa464d684fd04f1b32a3cb93dc2ae5f878652d Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 25 Jun 2024 14:08:08 -0700 Subject: [PATCH 14/17] add supported OS section --- java/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/java/README.md b/java/README.md index ca972b9276..6b23902faf 100644 --- a/java/README.md +++ b/java/README.md @@ -22,6 +22,10 @@ This preview release is recommended for testing purposes only. The beta release of GLIDE for Redis was tested on Intel x86_64 using Ubuntu 22.04.1, Amazon Linux 2023 (AL2023), and macOS 12.7. +## Supported Operating Systems + +GLIDE for Redis is supported in Ubuntu, CentOS, and MacOS. + ## Java supported version JDK 11+. From 855999e7dc3ee5183124ac50a6af7737f8ccecdb Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 26 Jun 2024 15:54:40 -0700 Subject: [PATCH 15/17] add set up information --- java/README.md | 103 +++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 59 deletions(-) diff --git a/java/README.md b/java/README.md index 6b23902faf..05535f5055 100644 --- a/java/README.md +++ b/java/README.md @@ -1,30 +1,30 @@ -# GLIDE for Redis +# GLIDE for Valkey -General Language Independent Driver for the Enterprise (GLIDE) for Redis, is an AWS-sponsored, open-source Redis client. GLIDE for Redis works with any Redis distribution that adheres to the Redis Serialization -Protocol (RESP) specification, including open-source Redis, Amazon ElastiCache for Redis, and Amazon MemoryDB for Redis. -Strategic, mission-critical Redis-based applications have requirements for security, optimized performance, minimal downtime, and observability. GLIDE for Redis is designed to provide a client experience that helps meet these objectives. -It is sponsored and supported by AWS, and comes pre-configured with best practices learned from over a decade of operating Redis-compatible services used by hundreds of thousands of customers. -To help ensure consistency in development and operations, GLIDE for Redis is implemented using a core driver framework, written in Rust, with extensions made available for each supported programming language. This design ensures that updates easily propagate to each language and reduces overall complexity. -In this release, GLIDE for Redis is available for Python, Javascript (Node.js), and Java. +General Language Independent Driver for the Enterprise (GLIDE) for Valkey, is an AWS-sponsored, open-source Valkey client. GLIDE for Valkey works with any Valkey distribution that adheres to the Valkey Serialization +Protocol (RESP) specification, including open-source Valkey, Amazon ElastiCache for Valkey, and Amazon MemoryDB for Valkey. +Strategic, mission-critical Valkey-based applications have requirements for security, optimized performance, minimal downtime, and observability. GLIDE for Valkey is designed to provide a client experience that helps meet these objectives. +It is sponsored and supported by AWS, and comes pre-configured with best practices learned from over a decade of operating Valkey-compatible services used by hundreds of thousands of customers. +To help ensure consistency in development and operations, GLIDE for Valkey is implemented using a core driver framework, written in Rust, with extensions made available for each supported programming language. This design ensures that updates easily propagate to each language and reduces overall complexity. +In this release, GLIDE for Valkey is available for Python, Javascript (Node.js), and Java. -## Supported Redis Versions +## Supported Valkey Versions -GLIDE for Redis is API-compatible with open source Redis version 6 and 7. +GLIDE for Valkey is API-compatible with open source Valkey version 6 and 7. ## Current Status -We've made GLIDE for Redis an open-source project, and are releasing it in Preview to the community to gather feedback, and actively collaborate on the project roadmap. We welcome questions and contributions from all Redis stakeholders. +We've made GLIDE for Valkey an open-source project, and are releasing it in Preview to the community to gather feedback, and actively collaborate on the project roadmap. We welcome questions and contributions from all Valkey stakeholders. This preview release is recommended for testing purposes only. # Getting Started - Java Wrapper ## System Requirements -The beta release of GLIDE for Redis was tested on Intel x86_64 using Ubuntu 22.04.1, Amazon Linux 2023 (AL2023), and macOS 12.7. +The beta release of GLIDE for Valkey was tested on Intel x86_64 using Ubuntu 22.04.1, Amazon Linux 2023 (AL2023), and macOS 12.7. ## Supported Operating Systems -GLIDE for Redis is supported in Ubuntu, CentOS, and MacOS. +GLIDE for Valkey is supported in Ubuntu, CentOS, and MacOS. ## Java supported version JDK 11+. @@ -33,8 +33,8 @@ The Java client contains the following parts: 1. `src`: Rust dynamic library FFI to integrate with [GLIDE core library](https://github.com/aws/glide-for-redis/blob/main/glide-core/README.md). 2. `client`: A Java-wrapper around the [GLIDE core rust library](../glide-core/README.md) and unit tests for it. -3. `examples`: An examples app to test the client against a Redis localhost. -4. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of GLIDE for Redis and other Java clients. +3. `examples`: An examples app to test the client against a Valkey localhost. +4. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of GLIDE for Valkey and other Java clients. 5. `integTest`: An integration test sub-project for API and E2E testing. ## Installation and Setup @@ -48,14 +48,9 @@ At the moment, the Java client must be built from source. Software Dependencies: - JDK 11+ -- git -- protoc (protobuf compiler) -- Rust Please also consider installing the following packages to build [GLIDE core rust library](../glide-core/README.md): -- GCC -- pkg-config - openssl - openssl-dev @@ -97,36 +92,6 @@ echo $JAVA_HOME java -version ``` -#### Building and installation steps -The Java client is currently a work in progress and offers no guarantees. Users should build at their own risk. - -Before starting this step, make sure you've installed all software requirements. -1. Clone the repository: -```bash -VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch -git clone --branch ${VERSION} https://github.com/aws/glide-for-redis.git -cd glide-for-redis -``` -2. Initialize git submodule: -```bash -git submodule update --init --recursive -``` -3. Generate protobuf files: -```bash -cd java/ -./gradlew :client:protobuf -``` -4. Build the client library: -```bash -cd java/ -./gradlew :client:build -``` -5. Run tests: -```bash -cd java/ -$ ./gradlew :client:test -``` - Other useful gradle developer commands: * `./gradlew :client:test` to run client unit tests * `./gradlew :integTest:test` to run client examples @@ -135,14 +100,34 @@ Other useful gradle developer commands: * `./gradlew :examples:run` to run client examples (make sure you have a running redis on port `6379`) * `./gradlew :benchmarks:run` to run performance benchmarks + +### Setting up the Driver + +Refer to https://central.sonatype.com/search?q=glide&namespace=software.amazon.glide for your specific system. +Once set up, you can run the basic examples. + +Gradle: +- Copy the snippet and paste it in the `build.gradle` dependencies section. + +Maven (AARCH_64) specific. +- **IMPORTANT** must include a `classifier` block. Please use this dependency block instead and add it to the pom.xml file. +```bash + + software.amazon.glide + glide-osx-aarch_64 + osx-aarch_64 + 0.4.2 + +``` + ## Basic Examples -### Standalone Redis: +### Standalone Valkey: ```java -import glide.api.RedisClient; +import glide.api.ValkeyClient; -RedisClient client = RedisClient.CreateClient().get(); +ValkeyClient client = ValkeyClient.CreateClient().get(); CompletableFuture setResponse = client.set("key", "foobar"); assert setResponse.get() == "OK" : "Failed on client.set("key", "foobar") request"; @@ -151,27 +136,27 @@ CompletableFuture getResponse = client.get("key"); assert getResponse.get() == "foobar" : "Failed on client.get("key") request"; ``` -### Cluster Redis: +### Cluster Valkey: ```java -import glide.api.RedisClusterClient; +import glide.api.ValkeyClusterClient; String host = "localhost"; Integer port = 6379; boolean useSsl = false; -RedisClientConfiguration config = - RedisClientConfiguration.builder() +ValkeyClientConfiguration config = + ValkeyClientConfiguration.builder() .address(NodeAddress.builder().host(host).port(port).build()) .useTLS(useSsl) .build(); -RedisClusterClient client = RedisClusterClient.CreateClient(config).get(); +ValkeyClusterClient client = ValkeyClusterClient.CreateClient(config).get(); CompletableFuture setResponse = client.set("key", "foobar"); -assert setResponse.get() == "OK" : "Failed on client.set("key", "foobar") request"; +assert setResponse.get() == "OK" : "Failed on client.set(\"key\", \"foobar\") request"; CompletableFuture getResponse = client.get("key"); -assert getResponse.get() == "foobar" : "Failed on client.get("key") request"; +assert getResponse.get() == "foobar" : "Failed on client.get(\"key\") request"; ``` ### Benchmarks From 268cda40d7564ba772da3768752b32c27deb3b84 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 26 Jun 2024 19:33:12 -0700 Subject: [PATCH 16/17] current progress --- java/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/java/README.md b/java/README.md index 05535f5055..f1a7c90404 100644 --- a/java/README.md +++ b/java/README.md @@ -108,6 +108,14 @@ Once set up, you can run the basic examples. Gradle: - Copy the snippet and paste it in the `build.gradle` dependencies section. +Example shown below is for `glide-osx-aarch_64`. +```bash +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + implementation group: 'software.amazon.glide', name: 'glide-osx-aarch_64', version: '0.4.2' +} +``` Maven (AARCH_64) specific. - **IMPORTANT** must include a `classifier` block. Please use this dependency block instead and add it to the pom.xml file. @@ -134,6 +142,9 @@ assert setResponse.get() == "OK" : "Failed on client.set("key", "foobar") reques CompletableFuture getResponse = client.get("key"); assert getResponse.get() == "foobar" : "Failed on client.get("key") request"; + +GlideString value = client.set(gs("key"), gs("value")).get(); +assert value.getString().equals("OK"); ``` ### Cluster Valkey: @@ -157,6 +168,9 @@ assert setResponse.get() == "OK" : "Failed on client.set(\"key\", \"foobar\") re CompletableFuture getResponse = client.get("key"); assert getResponse.get() == "foobar" : "Failed on client.get(\"key\") request"; + +GlideString value = client.set(gs("key"), gs("value")).get(); +assert value.getString().equals("OK"); ``` ### Benchmarks From a35414ebc63c351af5acc33dd1d6153e2acc7d10 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 26 Jun 2024 21:16:32 -0700 Subject: [PATCH 17/17] fix examples --- java/README.md | 64 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/java/README.md b/java/README.md index f1a7c90404..f0ea9a8ba1 100644 --- a/java/README.md +++ b/java/README.md @@ -133,44 +133,72 @@ Maven (AARCH_64) specific. ### Standalone Valkey: ```java -import glide.api.ValkeyClient; +import glide.api.RedisClient; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.RedisClientConfiguration; -ValkeyClient client = ValkeyClient.CreateClient().get(); +import java.util.concurrent.ExecutionException; +import static glide.api.models.GlideString.gs; -CompletableFuture setResponse = client.set("key", "foobar"); -assert setResponse.get() == "OK" : "Failed on client.set("key", "foobar") request"; +# Run this code in the Main file. Include InterruptedException and ExecutionException handling. -CompletableFuture getResponse = client.get("key"); -assert getResponse.get() == "foobar" : "Failed on client.get("key") request"; +public static void main(String[] args) throws InterruptedException, ExecutionException { -GlideString value = client.set(gs("key"), gs("value")).get(); -assert value.getString().equals("OK"); +String host = "localhost"; +Integer port = 6379; +boolean useSsl = false; + +RedisClientConfiguration config = + RedisClientConfiguration.builder() + .address(NodeAddress.builder().host(host).port(port).build()) + .useTLS(useSsl) + .build(); + +RedisClient client = RedisClient.CreateClient(config).get(); + +System.out.println("PING: " + client.ping().get()); +System.out.println("PING(found you): " + client.ping("found you").get()); + +System.out.println("SET(apples, oranges): " + client.set("apples", "oranges").get()); +System.out.println("GET(apples): " + client.get("apples").get()); + +System.out.println("GLIDESTRINGSET(cats, meow): " + client.set(gs("cats"), gs("meow")).get()); +System.out.println("GET(cats): " + client.get("cats").get()); +} ``` ### Cluster Valkey: ```java -import glide.api.ValkeyClusterClient; + +import glide.api.RedisClusterClient; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.RedisClusterClientConfiguration; + +import java.util.concurrent.ExecutionException; +import static glide.api.models.GlideString.gs; + +# Run this code in the Main file. Include InterruptedException and ExecutionException handling. String host = "localhost"; Integer port = 6379; boolean useSsl = false; -ValkeyClientConfiguration config = - ValkeyClientConfiguration.builder() +RedisClusterClientConfiguration config = + RedisClusterClientConfiguration.builder() .address(NodeAddress.builder().host(host).port(port).build()) .useTLS(useSsl) .build(); -ValkeyClusterClient client = ValkeyClusterClient.CreateClient(config).get(); +RedisClusterClient client = RedisClusterClient.CreateClient(config).get(); -CompletableFuture setResponse = client.set("key", "foobar"); -assert setResponse.get() == "OK" : "Failed on client.set(\"key\", \"foobar\") request"; +System.out.println("PING: " + client.ping().get()); +System.out.println("PING(found you): " + client.ping("found you").get()); -CompletableFuture getResponse = client.get("key"); -assert getResponse.get() == "foobar" : "Failed on client.get(\"key\") request"; +System.out.println("SET(apples, oranges): " + client.set("apples", "oranges").get()); +System.out.println("GET(apples): " + client.get("apples").get()); -GlideString value = client.set(gs("key"), gs("value")).get(); -assert value.getString().equals("OK"); +System.out.println("GLIDESTRINGSET(cats, meow): " + client.set(gs("cats"), gs("meow")).get()); +System.out.println("GET(cats): " + client.get("cats").get()); ``` ### Benchmarks