From 637f04b26f0ed1c828fada6c82b6018933f17b91 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Thu, 23 May 2024 16:41:19 -0700 Subject: [PATCH 1/6] changing sleep duration for ObjectIdletime Test (#1454) Co-authored-by: TJ Zhang --- java/integTest/src/test/java/glide/SharedCommandTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 6388da410c..06d7357a6d 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -3517,7 +3517,7 @@ public void objectIdletime_returns_null(BaseClient client) { public void objectIdletime(BaseClient client) { String key = UUID.randomUUID().toString(); assertEquals(OK, client.set(key, "").get()); - Thread.sleep(1000); + Thread.sleep(2000); assertTrue(client.objectIdletime(key).get() > 0L); } From 70e0f98cec90388ae256c4219d45e0fbb8ea6d9e Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 24 May 2024 10:24:20 -0700 Subject: [PATCH 2/6] Java: Add `HRANDFIELD` command. (#1455) Java: Add `HRANDFIELD` command. (#294) Signed-off-by: Yury-Fridlyand --- .../src/main/java/glide/api/BaseClient.java | 24 ++++++ .../glide/api/commands/HashBaseCommands.java | 60 +++++++++++++++ .../glide/api/models/BaseTransaction.java | 55 ++++++++++++++ .../test/java/glide/api/RedisClientTest.java | 74 +++++++++++++++++++ .../glide/api/models/TransactionTests.java | 10 +++ .../test/java/glide/SharedCommandTests.java | 56 ++++++++++++++ .../java/glide/TransactionTestUtilities.java | 12 +++ 7 files changed, 291 insertions(+) diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index b5d600a094..5194bc2750 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -38,6 +38,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.HKeys; import static redis_request.RedisRequestOuterClass.RequestType.HLen; import static redis_request.RedisRequestOuterClass.RequestType.HMGet; +import static redis_request.RedisRequestOuterClass.RequestType.HRandField; import static redis_request.RedisRequestOuterClass.RequestType.HSet; import static redis_request.RedisRequestOuterClass.RequestType.HSetNX; import static redis_request.RedisRequestOuterClass.RequestType.HVals; @@ -538,6 +539,29 @@ public CompletableFuture hkeys(@NonNull String key) { response -> castArray(handleArrayResponse(response), String.class)); } + @Override + public CompletableFuture hrandfield(@NonNull String key) { + return commandManager.submitNewCommand( + HRandField, new String[] {key}, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture hrandfieldWithCount(@NonNull String key, long count) { + return commandManager.submitNewCommand( + HRandField, + new String[] {key, Long.toString(count)}, + response -> castArray(handleArrayResponse(response), String.class)); + } + + @Override + public CompletableFuture hrandfieldWithCountWithValues( + @NonNull String key, long count) { + return commandManager.submitNewCommand( + HRandField, + new String[] {key, Long.toString(count), WITH_VALUES_REDIS_API}, + response -> castArrayofArrays(handleArrayResponse(response), String.class)); + } + @Override public CompletableFuture lpush(@NonNull String key, @NonNull String[] elements) { String[] arguments = ArrayUtils.addFirst(elements, 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 ac8caf02f4..cae1c4fae8 100644 --- a/java/client/src/main/java/glide/api/commands/HashBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/HashBaseCommands.java @@ -11,6 +11,8 @@ * @see Hash Commands */ public interface HashBaseCommands { + /** Redis API keyword used to query hash members with their values. */ + String WITH_VALUES_REDIS_API = "WITHVALUES"; /** * Retrieves the value associated with field in the hash stored at key. @@ -234,4 +236,62 @@ public interface HashBaseCommands { * } */ CompletableFuture hkeys(String key); + + /** + * Returns a random field name from the hash value stored at key. + * + * @since Redis 6.2 and above. + * @see redis.io for details. + * @param key The key of the hash. + * @return A random field name from the hash stored at key, or null when + * the key does not exist. + * @example + *
{@code
+     * String field = client.hrandfield("my_hash").get();
+     * System.out.printf("A random field from the hash is '%s'", field);
+     * }
+ */ + CompletableFuture hrandfield(String key); + + /** + * Retrieves up to count random field names from the hash value stored at key + * . + * + * @since Redis 6.2 and above. + * @see redis.io for details. + * @param key The key of the hash. + * @param count The number of field names to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates. + * @return An array of random field names from the hash stored at key, + * or an empty array when the key does not exist. + * @example + *
{@code
+     * String[] fields = client.hrandfieldWithCount("my_hash", 10).get();
+     * System.out.printf("Random fields from the hash are '%s'", String.join(", ", fields));
+     * }
+ */ + CompletableFuture hrandfieldWithCount(String key, long count); + + /** + * Retrieves up to count random field names along with their values from the hash + * value stored at key. + * + * @since Redis 6.2 and above. + * @see redis.io for details. + * @param key The key of the hash. + * @param count The number of field names to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates. + * @return A 2D array of [fieldName, value] arrays, where + * fieldName is a random field name from the hash and value is the + * associated value of the field name.
+ * If the hash does not exist or is empty, the response will be an empty array. + * @example + *
{@code
+     * String[][] fields = client.hrandfieldWithCountWithValues("my_hash", 1).get();
+     * System.out.printf("A random field from the hash is '%s' and the value is '%s'", fields[0][0], fields[0][1]);
+     * }
+ */ + CompletableFuture hrandfieldWithCountWithValues(String key, long count); } 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 876c6f946d..94694b4154 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.COUNT_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.LIMIT_REDIS_API; @@ -51,6 +52,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.HKeys; import static redis_request.RedisRequestOuterClass.RequestType.HLen; import static redis_request.RedisRequestOuterClass.RequestType.HMGet; +import static redis_request.RedisRequestOuterClass.RequestType.HRandField; import static redis_request.RedisRequestOuterClass.RequestType.HSet; import static redis_request.RedisRequestOuterClass.RequestType.HSetNX; import static redis_request.RedisRequestOuterClass.RequestType.HVals; @@ -707,6 +709,59 @@ public T hkeys(@NonNull String key) { return getThis(); } + /** + * Returns a random field name from the hash value stored at key. + * + * @since Redis 6.2 and above. + * @see redis.io for details. + * @param key The key of the hash. + * @return Command Response - A random field name from the hash stored at key, or + * null when the key does not exist. + */ + public T hrandfield(@NonNull String key) { + protobufTransaction.addCommands(buildCommand(HRandField, buildArgs(key))); + return getThis(); + } + + /** + * Retrieves up to count random field names from the hash value stored at key + * . + * + * @since Redis 6.2 and above. + * @see redis.io for details. + * @param key The key of the hash. + * @param count The number of field names to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates. + * @return Command Response - An array of random field names from the hash stored at + * key, or an empty array when the key does not exist. + */ + public T hrandfieldWithCount(@NonNull String key, long count) { + protobufTransaction.addCommands(buildCommand(HRandField, buildArgs(key, Long.toString(count)))); + return getThis(); + } + + /** + * Retrieves up to count random field names along with their values from the hash + * value stored at key. + * + * @since Redis 6.2 and above. + * @see redis.io for details. + * @param key The key of the hash. + * @param count The number of field names to return.
+ * If count is positive, returns unique elements.
+ * If negative, allows for duplicates. + * @return Command Response - A 2D array of [fieldName, value] + * arrays, where fieldName is a random field name from the hash and + * value is the associated value of the field name.
+ * If the hash does not exist or is empty, the response will be an empty array. + */ + public T hrandfieldWithCountWithValues(@NonNull String key, long count) { + ArgsArray commandArgs = buildArgs(key, Long.toString(count), WITH_VALUES_REDIS_API); + protobufTransaction.addCommands(buildCommand(HRandField, commandArgs)); + return getThis(); + } + /** * 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 diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 1560cb7509..2ce10fb9cc 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -2,6 +2,7 @@ package glide.api; import static glide.api.BaseClient.OK; +import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.LIMIT_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; @@ -72,6 +73,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.HKeys; import static redis_request.RedisRequestOuterClass.RequestType.HLen; import static redis_request.RedisRequestOuterClass.RequestType.HMGet; +import static redis_request.RedisRequestOuterClass.RequestType.HRandField; import static redis_request.RedisRequestOuterClass.RequestType.HSet; import static redis_request.RedisRequestOuterClass.RequestType.HSetNX; import static redis_request.RedisRequestOuterClass.RequestType.HVals; @@ -1421,6 +1423,78 @@ public void hkeys_returns_success() { assertEquals(values, payload); } + @SneakyThrows + @Test + public void hrandfield_returns_success() { + // setup + String key = "testKey"; + String[] args = {key}; + String field = "field"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(field); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HRandField), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hrandfield(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(field, payload); + } + + @SneakyThrows + @Test + public void hrandfieldWithCount_returns_success() { + // setup + String key = "testKey"; + String[] args = {key, "2"}; + String[] fields = new String[] {"field_1", "field_2"}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(fields); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HRandField), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hrandfieldWithCount(key, 2); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(fields, payload); + } + + @SneakyThrows + @Test + public void hrandfieldWithCountWithValues_returns_success() { + // setup + String key = "testKey"; + String[] args = {key, "2", WITH_VALUES_REDIS_API}; + String[][] fields = new String[][] {{"field_1", "value_1"}, {"field_2", "value_2"}}; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(fields); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(HRandField), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.hrandfieldWithCountWithValues(key, 2); + String[][] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(fields, payload); + } + @SneakyThrows @Test public void lpush_returns_success() { diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index a361888988..1027739c79 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.models; +import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API; import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.LIMIT_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; @@ -62,6 +63,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.HKeys; import static redis_request.RedisRequestOuterClass.RequestType.HLen; import static redis_request.RedisRequestOuterClass.RequestType.HMGet; +import static redis_request.RedisRequestOuterClass.RequestType.HRandField; import static redis_request.RedisRequestOuterClass.RequestType.HSet; import static redis_request.RedisRequestOuterClass.RequestType.HSetNX; import static redis_request.RedisRequestOuterClass.RequestType.HVals; @@ -288,6 +290,14 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.hkeys("key"); results.add(Pair.of(HKeys, buildArgs("key"))); + transaction + .hrandfield("key") + .hrandfieldWithCount("key", 2) + .hrandfieldWithCountWithValues("key", 3); + results.add(Pair.of(HRandField, buildArgs("key"))); + results.add(Pair.of(HRandField, buildArgs("key", "2"))); + results.add(Pair.of(HRandField, buildArgs("key", "3", WITH_VALUES_REDIS_API))); + transaction.lpush("key", new String[] {"element1", "element2"}); results.add(Pair.of(LPush, buildArgs("key", "element1", "element2"))); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 06d7357a6d..19806522c1 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -779,6 +779,62 @@ public void hkeys(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void hrandfield(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + // key does not exist + assertNull(client.hrandfield(key1).get()); + assertEquals(0, client.hrandfieldWithCount(key1, 5).get().length); + assertEquals(0, client.hrandfieldWithCountWithValues(key1, 5).get().length); + + var data = Map.of("f 1", "v 1", "f 2", "v 2", "f 3", "v 3"); + assertEquals(3, client.hset(key1, data).get()); + + // random key + assertTrue(data.containsKey(client.hrandfield(key1).get())); + + // WithCount - positive count + var keys = client.hrandfieldWithCount(key1, 5).get(); + assertEquals(data.keySet().size(), keys.length); + assertEquals(data.keySet(), Set.of(keys)); + + // WithCount - negative count + keys = client.hrandfieldWithCount(key1, -5).get(); + assertEquals(5, keys.length); + Arrays.stream(keys).forEach(key -> assertTrue(data.containsKey(key))); + + // WithCountWithValues - positive count + var keysWithValues = client.hrandfieldWithCountWithValues(key1, 5).get(); + assertEquals(data.keySet().size(), keysWithValues.length); + for (var pair : keysWithValues) { + assertEquals(data.get(pair[0]), pair[1]); + } + + // WithCountWithValues - negative count + keysWithValues = client.hrandfieldWithCountWithValues(key1, -5).get(); + assertEquals(5, keysWithValues.length); + for (var pair : keysWithValues) { + assertEquals(data.get(pair[0]), pair[1]); + } + + // Key exists, but it is not a List + assertEquals(OK, client.set(key2, "value").get()); + Exception executionException = + assertThrows(ExecutionException.class, () -> client.hrandfield(key2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.hrandfieldWithCount(key2, 2).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows( + ExecutionException.class, () -> client.hrandfieldWithCountWithValues(key2, 3).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index 5f760b3efe..acaa19915e 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -205,6 +205,11 @@ private static Object[] hashCommands(BaseTransaction transaction) { .hgetall(hashKey1) .hdel(hashKey1, new String[] {field1}) .hvals(hashKey1) + .hrandfield(hashKey1) + .hrandfieldWithCount(hashKey1, 2) + .hrandfieldWithCount(hashKey1, -2) + .hrandfieldWithCountWithValues(hashKey1, 2) + .hrandfieldWithCountWithValues(hashKey1, -2) .hincrBy(hashKey1, field3, 5) .hincrByFloat(hashKey1, field3, 5.5) .hkeys(hashKey1); @@ -219,6 +224,13 @@ private static Object[] hashCommands(BaseTransaction transaction) { Map.of(field1, value1, field2, value2), // hgetall(hashKey1) 1L, // hdel(hashKey1, new String[] {field1}) new String[] {value2}, // hvals(hashKey1) + field2, // hrandfield(hashKey1) + new String[] {field2}, // hrandfieldWithCount(hashKey1, 2) + new String[] {field2, field2}, // hrandfieldWithCount(hashKey1, -2) + new String[][] {{field2, value2}}, // hrandfieldWithCountWithValues(hashKey1, 2) + new String[][] { + {field2, value2}, {field2, value2} + }, // hrandfieldWithCountWithValues(hashKey1, -2) 5L, // hincrBy(hashKey1, field3, 5) 10.5, // hincrByFloat(hashKey1, field3, 5.5) new String[] {field2, field3}, // hkeys(hashKey1) From 16a08e1511263d22b0db8e39895bac3fce007775 Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Fri, 24 May 2024 11:19:51 -0700 Subject: [PATCH 3/6] Python: add SINTERSTORE command (#1459) * Python: add SINTERSTORE command (#304) * Update CHANGELOG * Fix incorrect valkey doc links --- CHANGELOG.md | 1 + python/python/glide/async_commands/core.py | 37 +++++++++++++--- .../glide/async_commands/transaction.py | 21 ++++++++- python/python/tests/test_async_client.py | 44 +++++++++++++++++++ python/python/tests/test_transaction.py | 2 + 5 files changed, 98 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b028ee3bc9..ac863da441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ * Python: Added SINTER command ([#1434](https://github.com/aws/glide-for-redis/pull/1434)) * Python: Added SDIFF command ([#1437](https://github.com/aws/glide-for-redis/pull/1437)) * Python: Added SDIFFSTORE command ([#1449](https://github.com/aws/glide-for-redis/pull/1449)) +* Python: Added SINTERSTORE command ([#1459](https://github.com/aws/glide-for-redis/pull/1459)) #### Fixes diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 96c11b6852..ac65704f62 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -1606,7 +1606,7 @@ async def sdiffstore(self, destination: str, keys: List[str]) -> int: Stores the difference between the first set and all the successive sets in `keys` into a new set at `destination`. - See https://valkey.io/docs/latest/commands/sdiffstore for more details. + See https://valkey.io/commands/sdiffstore for more details. Note: When in Cluster mode, all keys in `keys` and `destination` must map to the same hash slot. @@ -1633,14 +1633,14 @@ async def sinter(self, keys: List[str]) -> Set[str]: """ Gets the intersection of all the given sets. - See https://valkey.io/docs/latest/commands/sinter for more details. - - Args: - keys (List[str]): The keys of the sets. + See https://valkey.io/commands/sinter for more details. Note: When in cluster mode, all `keys` must map to the same hash slot. + Args: + keys (List[str]): The keys of the sets. + Returns: Set[str]: A set of members which are present in all given sets. If one or more sets do no exist, an empty set will be returned. @@ -1655,6 +1655,33 @@ async def sinter(self, keys: List[str]) -> Set[str]: """ return cast(Set[str], await self._execute_command(RequestType.SInter, keys)) + async def sinterstore(self, destination: str, keys: List[str]) -> int: + """ + Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination`. + + See https://valkey.io/commands/sinterstore for more details. + + Note: + When in Cluster mode, all `keys` and `destination` must map to the same hash slot. + + Args: + destination (str): The key of the destination set. + keys (List[str]): The keys from which to retrieve the set members. + + Returns: + int: The number of elements in the resulting set. + + Examples: + >>> await client.sadd("my_set1", ["member1", "member2"]) + >>> await client.sadd("my_set2", ["member2", "member3"]) + >>> await client.sinterstore("my_set3", ["my_set1", "my_set2"]) + 1 # One element was stored at "my_set3", and that element is the intersection of "my_set1" and "myset2". + """ + return cast( + int, + await self._execute_command(RequestType.SInterStore, [destination] + keys), + ) + async def sdiff(self, keys: List[str]) -> Set[str]: """ Computes the difference between the first set and all the successive sets in `keys`. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 3dd9d03e87..e852ba7d7c 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -1056,7 +1056,7 @@ def sinter(self: TTransaction, keys: List[str]) -> TTransaction: """ Gets the intersection of all the given sets. - See https://valkey.io/docs/latest/commands/sinter for more details. + See https://valkey.io/commands/sinter for more details. Args: keys (List[str]): The keys of the sets. @@ -1067,6 +1067,23 @@ def sinter(self: TTransaction, keys: List[str]) -> TTransaction: """ return self.append_command(RequestType.SInter, keys) + def sinterstore( + self: TTransaction, destination: str, keys: List[str] + ) -> TTransaction: + """ + Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination`. + + See https://valkey.io/commands/sinterstore for more details. + + Args: + destination (str): The key of the destination set. + keys (List[str]): The keys from which to retrieve the set members. + + Command response: + int: The number of elements in the resulting set. + """ + return self.append_command(RequestType.SInterStore, [destination] + keys) + def sdiff(self: TTransaction, keys: List[str]) -> TTransaction: """ Computes the difference between the first set and all the successive sets in `keys`. @@ -1089,7 +1106,7 @@ def sdiffstore( Stores the difference between the first set and all the successive sets in `keys` into a new set at `destination`. - See https://valkey.io/docs/latest/commands/sdiffstore for more details. + See https://valkey.io/commands/sdiffstore for more details. Args: destination (str): The key of the destination set. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index c656ad60ac..09845e3085 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -1216,6 +1216,49 @@ async def test_sinter(self, redis_client: TRedisClient): await redis_client.sinter([key2]) assert "Operation against a key holding the wrong kind of value" in str(e) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_sinterstore(self, redis_client: TRedisClient): + key1 = f"{{testKey}}:{get_random_string(10)}" + key2 = f"{{testKey}}:{get_random_string(10)}" + key3 = f"{{testKey}}:{get_random_string(10)}" + string_key = f"{{testKey}}:{get_random_string(10)}" + non_existing_key = f"{{testKey}}:non_existing_key" + member1_list = ["a", "b", "c"] + member2_list = ["c", "d", "e"] + + assert await redis_client.sadd(key1, member1_list) == 3 + assert await redis_client.sadd(key2, member2_list) == 3 + + # store in new key + assert await redis_client.sinterstore(key3, [key1, key2]) == 1 + assert await redis_client.smembers(key3) == {"c"} + + # overwrite existing set, which is also a source set + assert await redis_client.sinterstore(key2, [key2, key3]) == 1 + assert await redis_client.smembers(key2) == {"c"} + + # source set is the same as the existing set + assert await redis_client.sinterstore(key2, [key2]) == 1 + assert await redis_client.smembers(key2) == {"c"} + + # intersection with non-existing key + assert await redis_client.sinterstore(key1, [key2, non_existing_key]) == 0 + assert await redis_client.smembers(key1) == set() + + # invalid argument - key list must not be empty + with pytest.raises(RequestError): + await redis_client.sinterstore(key3, []) + + # non-set key + assert await redis_client.set(string_key, "value") == OK + with pytest.raises(RequestError) as e: + await redis_client.sinterstore(key3, [string_key]) + + # overwrite non-set key + assert await redis_client.sinterstore(string_key, [key2]) == 1 + assert await redis_client.smembers(string_key) == {"c"} + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_sdiff(self, redis_client: TRedisClient): @@ -3227,6 +3270,7 @@ async def test_multi_key_command_returns_cross_slot_error( redis_client.smove("abc", "def", "_"), redis_client.sunionstore("abc", ["zxy", "lkn"]), redis_client.sinter(["abc", "zxy", "lkn"]), + redis_client.sinterstore("abc", ["zxy", "lkn"]), redis_client.sdiff(["abc", "zxy", "lkn"]), redis_client.sdiffstore("abc", ["def", "ghi"]), ] diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index ffbdcd3ad0..a48a499d1d 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -209,6 +209,8 @@ async def transaction_test( args.append(2) transaction.sinter([key7, key7]) args.append({"foo", "bar"}) + transaction.sinterstore(key7, [key7, key7]) + args.append(2) transaction.sdiff([key7, key7]) args.append(set()) transaction.spop_count(key7, 4) From 619eb23b1fd488e69ba490f47a0a73fa187c0d54 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 24 May 2024 12:37:16 -0700 Subject: [PATCH 4/6] Java: Add `ZINCRBY` command. (#1460) * Add `ZINCRBY` command. (#296) Signed-off-by: Yury-Fridlyand --- glide-core/src/client/value_conversion.rs | 2 +- .../src/main/java/glide/api/BaseClient.java | 8 +++++ .../api/commands/SortedSetBaseCommands.java | 29 ++++++++++++++++--- .../glide/api/models/BaseTransaction.java | 25 ++++++++++++++-- .../test/java/glide/api/RedisClientTest.java | 27 +++++++++++++++++ .../glide/api/models/TransactionTests.java | 4 +++ .../test/java/glide/SharedCommandTests.java | 26 +++++++++++++++++ .../java/glide/TransactionTestUtilities.java | 2 ++ 8 files changed, 116 insertions(+), 7 deletions(-) diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index f58811ba4e..02652ff6f5 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -497,7 +497,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { b"HGETALL" | b"XREAD" | b"CONFIG GET" | b"FT.CONFIG GET" | b"HELLO" => { Some(ExpectedReturnType::Map) } - b"INCRBYFLOAT" | b"HINCRBYFLOAT" => Some(ExpectedReturnType::Double), + b"INCRBYFLOAT" | b"HINCRBYFLOAT" | b"ZINCRBY" => Some(ExpectedReturnType::Double), b"HEXISTS" | b"HSETNX" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT" | b"SISMEMBER" | b"PERSIST" | b"SMOVE" | b"RENAMENX" => Some(ExpectedReturnType::Boolean), b"SMISMEMBER" => Some(ExpectedReturnType::ArrayOfBools), diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 5194bc2750..7ac4107e20 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -98,6 +98,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZCount; import static redis_request.RedisRequestOuterClass.RequestType.ZDiff; import static redis_request.RedisRequestOuterClass.RequestType.ZDiffStore; +import static redis_request.RedisRequestOuterClass.RequestType.ZIncrBy; import static redis_request.RedisRequestOuterClass.RequestType.ZInter; import static redis_request.RedisRequestOuterClass.RequestType.ZInterCard; import static redis_request.RedisRequestOuterClass.RequestType.ZInterStore; @@ -1141,6 +1142,13 @@ public CompletableFuture zrandmemberWithCountWithScores( response -> castArray(handleArrayResponse(response), Object[].class)); } + @Override + public CompletableFuture zincrby( + @NonNull String key, double increment, @NonNull String member) { + String[] arguments = new String[] {key, Double.toString(increment), 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 d7a922d3d2..9dcb6b96d8 100644 --- a/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SortedSetBaseCommands.java @@ -130,7 +130,8 @@ CompletableFuture zadd( * If member does not exist in the sorted set, it is added with * increment as its score (as if its previous score was 0.0).
* If key does not exist, a new sorted set with the specified member as its sole - * member is created. + * member is created.
+ * zaddIncr with empty option acts as {@link #zincrby(String, double, String)}. * * @see redis.io for more details. * @param key The key of the sorted set. @@ -142,11 +143,11 @@ CompletableFuture zadd( * returned. * @example *
{@code
-     * ZaddOptions options = ZaddOptions.builder().conditionalChange(ONLY_IF_DOES_NOT_EXIST).build();
+     * ZAddOptions options = ZaddOptions.builder().conditionalChange(ONLY_IF_DOES_NOT_EXIST).build();
      * Double num = client.zaddIncr("mySortedSet", member, 5.0, options).get();
      * assert num == 5.0;
      *
-     * options = ZaddOptions.builder().updateOptions(SCORE_LESS_THAN_CURRENT).build();
+     * options = ZAddOptions.builder().updateOptions(SCORE_LESS_THAN_CURRENT).build();
      * Double num = client.zaddIncr("existingSortedSet", member, 3.0, options).get();
      * assert num == null;
      * }
@@ -1377,7 +1378,7 @@ CompletableFuture> zinterWithScores( * . * @example *
{@code
-     * Object[][] data = client.zrandmemberWithCountWithScores(key1, -3).get();
+     * Object[][] data = client.zrandmemberWithCountWithScores("mySortedSet", -3).get();
      * assert data.length == 3;
      * for (Object[] memberScorePair : data) {
      *     System.out.printf("Member: '%s', score: %d", memberScorePair[0], memberScorePair[1]);
@@ -1386,6 +1387,26 @@ CompletableFuture> zinterWithScores(
      */
     CompletableFuture zrandmemberWithCountWithScores(String key, long count);
 
+    /**
+     * 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("mySortedSet", -3.14, "value").get();
+     * assert score > 0; // member "value" existed in the set before score was altered
+     * }
+ */ + CompletableFuture zincrby(String key, double increment, String member); + /** * Returns the cardinality of the intersection of the sorted sets specified by keys. * 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 94694b4154..149b773677 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -117,6 +117,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZCount; import static redis_request.RedisRequestOuterClass.RequestType.ZDiff; import static redis_request.RedisRequestOuterClass.RequestType.ZDiffStore; +import static redis_request.RedisRequestOuterClass.RequestType.ZIncrBy; import static redis_request.RedisRequestOuterClass.RequestType.ZInter; import static redis_request.RedisRequestOuterClass.RequestType.ZInterCard; import static redis_request.RedisRequestOuterClass.RequestType.ZInterStore; @@ -1566,7 +1567,8 @@ public T zadd(@NonNull String key, @NonNull Map membersScoresMap * If member does not exist in the sorted set, it is added with * increment as its score (as if its previous score was 0.0).
* If key does not exist, a new sorted set with the specified member as its sole - * member is created. + * member is created.
+ * zaddIncr with empty option acts as {@link #zincrby(String, double, String)}. * * @see redis.io for more details. * @param key The key of the sorted set. @@ -1575,7 +1577,7 @@ public T zadd(@NonNull String key, @NonNull Map membersScoresMap * @param options The ZAdd options. * @return Command Response - The score of the member.
* If there was a conflict with the options, the operation aborts and null is - * returned.
+ * returned. */ public T zaddIncr( @NonNull String key, @NonNull String member, double increment, @NonNull ZAddOptions options) { @@ -1732,6 +1734,25 @@ public T zrandmemberWithCountWithScores(String key, long count) { return getThis(); } + /** + * 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 Command Response - The new score of member. + */ + public T zincrby(@NonNull String key, double increment, @NonNull String member) { + ArgsArray commandArgs = buildArgs(key, Double.toString(increment), member); + protobufTransaction.addCommands(buildCommand(ZIncrBy, commandArgs)); + return getThis(); + } + /** * Blocks the connection until it removes and returns a member with the lowest score from the * sorted sets stored at the specified keys. The sorted sets are checked in the order diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 2ce10fb9cc..72d0439599 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -138,6 +138,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZCount; import static redis_request.RedisRequestOuterClass.RequestType.ZDiff; import static redis_request.RedisRequestOuterClass.RequestType.ZDiffStore; +import static redis_request.RedisRequestOuterClass.RequestType.ZIncrBy; import static redis_request.RedisRequestOuterClass.RequestType.ZInter; import static redis_request.RedisRequestOuterClass.RequestType.ZInterCard; import static redis_request.RedisRequestOuterClass.RequestType.ZInterStore; @@ -3687,6 +3688,32 @@ public void zrandmemberWithCountWithScores_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void zincrby_returns_success() { + // setup + String key = "testKey"; + double increment = 4.2; + String member = "member"; + String[] arguments = new String[] {key, "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); + } + private static List getStreamAddOptions() { return List.of( Arguments.of( 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 1027739c79..c5208a4833 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -128,6 +128,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ZCount; import static redis_request.RedisRequestOuterClass.RequestType.ZDiff; import static redis_request.RedisRequestOuterClass.RequestType.ZDiffStore; +import static redis_request.RedisRequestOuterClass.RequestType.ZIncrBy; import static redis_request.RedisRequestOuterClass.RequestType.ZInter; import static redis_request.RedisRequestOuterClass.RequestType.ZInterCard; import static redis_request.RedisRequestOuterClass.RequestType.ZInterStore; @@ -692,6 +693,9 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), .addArgs(WITH_SCORES_REDIS_API) .build())); + transaction.zincrby("key", 3.14, "value"); + results.add(Pair.of(ZIncrBy, buildArgs("key", "3.14", "value"))); + transaction.type("key"); results.add(Pair.of(Type, buildArgs("key"))); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 19806522c1..62302b4b74 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -3062,6 +3062,32 @@ public void zrandmemberWithCountWithScores(BaseClient client) { assertInstanceOf(RequestException.class, executionException.getCause()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void zincrby(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + + // key does not exist + assertEquals(2.5, client.zincrby(key1, 2.5, "value1").get()); + assertEquals(2.5, client.zscore(key1, "value1").get()); + + // key exists, but value doesn't + assertEquals(-3.3, client.zincrby(key1, -3.3, "value2").get()); + assertEquals(-3.3, client.zscore(key1, "value2").get()); + + // updating existing value in existing key + assertEquals(3.5, client.zincrby(key1, 1., "value1").get()); + assertEquals(3.5, client.zscore(key1, "value1").get()); + + // Key exists, but it is not a sorted set + assertEquals(2L, client.sadd(key2, new String[] {"one", "two"}).get()); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.zincrby(key2, .5, "_").get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index acaa19915e..e170b598f3 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -329,6 +329,7 @@ private static Object[] sortedSetCommands(BaseTransaction transaction) { .zrank(zSetKey1, "one") .zrevrank(zSetKey1, "one") .zaddIncr(zSetKey1, "one", 3) + .zincrby(zSetKey1, -3., "one") .zrem(zSetKey1, new String[] {"one"}) .zcard(zSetKey1) .zmscore(zSetKey1, new String[] {"two", "three"}) @@ -384,6 +385,7 @@ private static Object[] sortedSetCommands(BaseTransaction transaction) { 0L, // zrank(zSetKey1, "one") 2L, // zrevrank(zSetKey1, "one") 4.0, // zaddIncr(zSetKey1, "one", 3) + 1., // zincrby(zSetKey1, -3.3, "one") 1L, // zrem(zSetKey1, new String[] {"one"}) 2L, // zcard(zSetKey1) new Double[] {2.0, 3.0}, // zmscore(zSetKey1, new String[] {"two", "three"}) From 6d75ec3c7dcfd30695be6969b6939c990b270089 Mon Sep 17 00:00:00 2001 From: Aaron <69273634+aaron-congo@users.noreply.github.com> Date: Fri, 24 May 2024 13:08:51 -0700 Subject: [PATCH 5/6] Python: add SMISMEMBER command (#1461) Python: add SMISMEMBER command (#306) --- CHANGELOG.md | 1 + python/python/glide/async_commands/core.py | 23 +++++++++++++++++++ .../glide/async_commands/transaction.py | 15 ++++++++++++ python/python/tests/test_async_client.py | 21 +++++++++++++++++ python/python/tests/test_transaction.py | 2 ++ 5 files changed, 62 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac863da441..8ce6beafea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ * Python: Added SDIFF command ([#1437](https://github.com/aws/glide-for-redis/pull/1437)) * Python: Added SDIFFSTORE command ([#1449](https://github.com/aws/glide-for-redis/pull/1449)) * Python: Added SINTERSTORE command ([#1459](https://github.com/aws/glide-for-redis/pull/1459)) +* Python: Added SMISMEMBER command ([#1461](https://github.com/aws/glide-for-redis/pull/1461)) #### Fixes diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index ac65704f62..a5a8420819 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -1709,6 +1709,29 @@ async def sdiff(self, keys: List[str]) -> Set[str]: await self._execute_command(RequestType.SDiff, keys), ) + async def smismember(self, key: str, members: List[str]) -> List[bool]: + """ + Checks whether each member is contained in the members of the set stored at `key`. + + See https://valkey.io/commands/smismember for more details. + + Args: + key (str): The key of the set to check. + members (List[str]): A list of members to check for existence in the set. + + Returns: + List[bool]: A list of bool values, each indicating if the respective member exists in the set. + + Examples: + >>> await client.sadd("set1", ["a", "b", "c"]) + >>> await client.smismember("set1", ["b", "c", "d"]) + [True, True, False] # "b" and "c" are members of "set1", but "d" is not. + """ + return cast( + List[bool], + await self._execute_command(RequestType.SMIsMember, [key] + members), + ) + async def ltrim(self, key: str, start: int, end: int) -> TOK: """ Trim an existing list so that it will contain only the specified range of elements specified. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index e852ba7d7c..8b780e6072 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -1117,6 +1117,21 @@ def sdiffstore( """ return self.append_command(RequestType.SDiffStore, [destination] + keys) + def smismember(self: TTransaction, key: str, members: List[str]) -> TTransaction: + """ + Checks whether each member is contained in the members of the set stored at `key`. + + See https://valkey.io/commands/smismember for more details. + + Args: + key (str): The key of the set to check. + members (List[str]): A list of members to check for existence in the set. + + Command response: + List[bool]: A list of bool values, each indicating if the respective member exists in the set. + """ + return self.append_command(RequestType.SMIsMember, [key] + members) + def ltrim(self: TTransaction, key: str, start: int, end: int) -> TTransaction: """ Trim an existing list so that it will contain only the specified range of elements specified. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 09845e3085..b54e37f76a 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -1330,6 +1330,27 @@ async def test_sdiffstore(self, redis_client: TRedisClient): assert await redis_client.sdiffstore(string_key, [key1, key2]) == 2 assert await redis_client.smembers(string_key) == {"a", "b"} + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_smismember(self, redis_client: TRedisClient): + key1 = get_random_string(10) + string_key = get_random_string(10) + non_existing_key = get_random_string(10) + + assert await redis_client.sadd(key1, ["one", "two"]) == 2 + assert await redis_client.smismember(key1, ["two", "three"]) == [True, False] + + assert await redis_client.smismember(non_existing_key, ["two"]) == [False] + + # invalid argument - member list must not be empty + with pytest.raises(RequestError): + await redis_client.smismember(key1, []) + + # source key exists, but it is not a set + assert await redis_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await redis_client.smismember(string_key, ["two"]) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_ltrim(self, redis_client: TRedisClient): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index a48a499d1d..c5071d8239 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -191,6 +191,8 @@ async def transaction_test( transaction.sadd(key7, ["foo", "bar"]) args.append(2) + transaction.smismember(key7, ["foo", "baz"]) + args.append([True, False]) transaction.sdiffstore(key7, [key7]) args.append(2) transaction.srem(key7, ["foo"]) From 7e1e04d057815a0d61855629cec0251dd5afbbc1 Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Fri, 24 May 2024 20:46:24 +0000 Subject: [PATCH 6/6] Java: Add `BITOP` command (#1458) * Java: Add `BITOP` command (#303) * Incomplete implementation * Needs more tests and javadocs * Fixed errors from rebasing * Added tests and javadocs * Fixed incorrect comment * Added tests * Fixed tests * Spotless * Added to module-info * Addressed PR comments * Added TODO: --- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + .../src/main/java/glide/api/BaseClient.java | 12 +++ .../api/commands/BitmapBaseCommands.java | 24 ++++++ .../glide/api/models/BaseTransaction.java | 23 ++++++ .../commands/bitmap/BitwiseOperation.java | 17 ++++ java/client/src/main/java/module-info.java | 1 + .../test/java/glide/api/RedisClientTest.java | 27 +++++++ .../glide/api/models/TransactionTests.java | 5 ++ .../test/java/glide/SharedCommandTests.java | 79 +++++++++++++++++++ .../java/glide/TransactionTestUtilities.java | 11 ++- .../test/java/glide/cluster/CommandTests.java | 7 +- 12 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 java/client/src/main/java/glide/api/models/commands/bitmap/BitwiseOperation.java diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 8095d51764..866c0196da 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -187,6 +187,7 @@ enum RequestType { GetBit = 145; ZInter = 146; BitPos = 147; + BitOp = 148; FunctionLoad = 150; } diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 8fa1bbb1ea..92fad77c5c 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -157,6 +157,7 @@ pub enum RequestType { GetBit = 145, ZInter = 146, BitPos = 147, + BitOp = 148, FunctionLoad = 150, } @@ -319,6 +320,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::ZInter => RequestType::ZInter, ProtobufRequestType::FunctionLoad => RequestType::FunctionLoad, ProtobufRequestType::BitPos => RequestType::BitPos, + ProtobufRequestType::BitOp => RequestType::BitOp, } } } @@ -476,6 +478,7 @@ impl RequestType { RequestType::ZInter => Some(cmd("ZINTER")), RequestType::FunctionLoad => Some(get_two_word_command("FUNCTION", "LOAD")), RequestType::BitPos => Some(cmd("BITPOS")), + RequestType::BitOp => Some(cmd("BITOP")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 7ac4107e20..f8333bc711 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -15,6 +15,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.BZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.BZPopMin; import static redis_request.RedisRequestOuterClass.RequestType.BitCount; +import static redis_request.RedisRequestOuterClass.RequestType.BitOp; import static redis_request.RedisRequestOuterClass.RequestType.BitPos; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; @@ -145,6 +146,7 @@ import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeys; import glide.api.models.commands.ZAddOptions; import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.geospatial.GeoAddOptions; import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; @@ -1442,4 +1444,14 @@ public CompletableFuture bitpos( }; return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse); } + + @Override + public CompletableFuture bitop( + @NonNull BitwiseOperation bitwiseOperation, + @NonNull String destination, + @NonNull String[] keys) { + String[] arguments = + concatenateArrays(new String[] {bitwiseOperation.toString(), destination}, keys); + return commandManager.submitNewCommand(BitOp, arguments, this::handleLongResponse); + } } 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 a613d488a3..0b15ad79f3 100644 --- a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java @@ -2,6 +2,7 @@ package glide.api.commands; import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; import java.util.concurrent.CompletableFuture; /** @@ -213,4 +214,27 @@ public interface BitmapBaseCommands { */ CompletableFuture bitpos( String 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. + * + * @apiNote When in cluster mode, destination and all keys must map to + * the same hash slot. + * @see redis.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, "destination", new String[] {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, String destination, String[] keys); } 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 149b773677..227e90ec7c 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -19,6 +19,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.BZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.BZPopMin; import static redis_request.RedisRequestOuterClass.RequestType.BitCount; +import static redis_request.RedisRequestOuterClass.RequestType.BitOp; import static redis_request.RedisRequestOuterClass.RequestType.BitPos; import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; import static redis_request.RedisRequestOuterClass.RequestType.ClientId; @@ -167,6 +168,7 @@ import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; import glide.api.models.commands.ZAddOptions; 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.geospatial.GeoAddOptions; import glide.api.models.commands.geospatial.GeoUnit; @@ -3529,6 +3531,27 @@ public T bitpos( return getThis(); } + /** + * Perform a bitwise operation between multiple keys (containing string values) and store the + * result in the destination. + * + * @see redis.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 Command Response - The size of the string stored in destination. + */ + public T bitop( + @NonNull BitwiseOperation bitwiseOperation, + @NonNull String destination, + @NonNull String[] keys) { + ArgsArray commandArgs = + buildArgs(concatenateArrays(new String[] {bitwiseOperation.toString(), destination}, keys)); + + protobufTransaction.addCommands(buildCommand(BitOp, 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/commands/bitmap/BitwiseOperation.java b/java/client/src/main/java/glide/api/models/commands/bitmap/BitwiseOperation.java new file mode 100644 index 0000000000..156b6bb556 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/bitmap/BitwiseOperation.java @@ -0,0 +1,17 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.bitmap; + +import glide.api.commands.BitmapBaseCommands; + +/** + * Defines bitwise operation for {@link BitmapBaseCommands#bitop(BitwiseOperation, String, + * String[])}. Specifies bitwise operation to perform between keys. + * + * @see redis.io + */ +public enum BitwiseOperation { + AND, + OR, + XOR, + NOT +} diff --git a/java/client/src/main/java/module-info.java b/java/client/src/main/java/module-info.java index 5bacdb40b8..2bd9fec690 100644 --- a/java/client/src/main/java/module-info.java +++ b/java/client/src/main/java/module-info.java @@ -6,6 +6,7 @@ exports glide.api.models.commands.stream; exports glide.api.models.configuration; exports glide.api.models.exceptions; + exports glide.api.models.commands.bitmap; exports glide.api.models.commands.geospatial; exports glide.api.models.commands.function; diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 72d0439599..f68b2195cf 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -40,6 +40,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.BZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.BZPopMin; import static redis_request.RedisRequestOuterClass.RequestType.BitCount; +import static redis_request.RedisRequestOuterClass.RequestType.BitOp; import static redis_request.RedisRequestOuterClass.RequestType.BitPos; import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; import static redis_request.RedisRequestOuterClass.RequestType.ClientId; @@ -182,6 +183,7 @@ import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; import glide.api.models.commands.ZAddOptions; 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.geospatial.GeoAddOptions; import glide.api.models.commands.geospatial.GeoUnit; @@ -4874,4 +4876,29 @@ public void bitpos_with_start_and_end_and_type_returns_success() { assertEquals(testResponse, response); assertEquals(bitPosition, payload); } + + @SneakyThrows + @Test + public void bitop_returns_success() { + // setup + String destination = "destination"; + String[] keys = new String[] {"key1", "key2"}; + Long result = 6L; + BitwiseOperation bitwiseAnd = BitwiseOperation.AND; + String[] arguments = concatenateArrays(new String[] {bitwiseAnd.toString(), 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); + } } 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 c5208a4833..828339d91e 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -31,6 +31,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.BZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.BZPopMin; import static redis_request.RedisRequestOuterClass.RequestType.BitCount; +import static redis_request.RedisRequestOuterClass.RequestType.BitOp; import static redis_request.RedisRequestOuterClass.RequestType.BitPos; import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; import static redis_request.RedisRequestOuterClass.RequestType.ClientId; @@ -166,6 +167,7 @@ import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; import glide.api.models.commands.ZAddOptions; import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.geospatial.GeoAddOptions; import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; @@ -823,6 +825,9 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.bitpos("key", 1, 8, 10, BitmapIndexType.BIT); results.add(Pair.of(BitPos, buildArgs("key", "1", "8", "10", BitmapIndexType.BIT.toString()))); + transaction.bitop(BitwiseOperation.AND, "destination", new String[] {"key"}); + results.add(Pair.of(BitOp, buildArgs(BitwiseOperation.AND.toString(), "destination", "key"))); + 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 62302b4b74..ca2f1dbcea 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -47,6 +47,7 @@ import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; import glide.api.models.commands.ZAddOptions; import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.geospatial.GeoAddOptions; import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; @@ -3982,4 +3983,82 @@ public void bitpos(BaseClient client) { assertTrue(executionException.getCause() instanceof RequestException); } } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bitop(BaseClient client) { + String key1 = "{key}-1".concat(UUID.randomUUID().toString()); + String key2 = "{key}-2".concat(UUID.randomUUID().toString()); + String emptyKey1 = "{key}-3".concat(UUID.randomUUID().toString()); + String emptyKey2 = "{key}-4".concat(UUID.randomUUID().toString()); + String destination = "{key}-5".concat(UUID.randomUUID().toString()); + String[] keys = new String[] {key1, key2}; + String[] emptyKeys = new String[] {emptyKey1, emptyKey2}; + String value1 = "foobar"; + String value2 = "abcdef"; + + assertEquals(OK, client.set(key1, value1).get()); + assertEquals(OK, client.set(key2, value2).get()); + assertEquals(6L, client.bitop(BitwiseOperation.AND, destination, keys).get()); + assertEquals("`bc`ab", client.get(destination).get()); + assertEquals(6L, client.bitop(BitwiseOperation.OR, destination, keys).get()); + assertEquals("goofev", client.get(destination).get()); + + // Reset values for simplicity of results in XOR + assertEquals(OK, client.set(key1, "a").get()); + assertEquals(OK, client.set(key2, "b").get()); + assertEquals(1L, client.bitop(BitwiseOperation.XOR, destination, keys).get()); + assertEquals("\u0003", client.get(destination).get()); + + // Test single source key + assertEquals(1L, client.bitop(BitwiseOperation.AND, destination, new String[] {key1}).get()); + assertEquals("a", client.get(destination).get()); + assertEquals(1L, client.bitop(BitwiseOperation.OR, destination, new String[] {key1}).get()); + assertEquals("a", client.get(destination).get()); + assertEquals(1L, client.bitop(BitwiseOperation.XOR, destination, new String[] {key1}).get()); + assertEquals("a", client.get(destination).get()); + assertEquals(1L, client.bitop(BitwiseOperation.NOT, destination, new String[] {key1}).get()); + // First bit is flipped to 1 and throws 'utf-8' codec can't decode byte 0x9e in position 0: + // invalid start byte + // TODO: update once fix is implemented for https://github.com/aws/glide-for-redis/issues/1447 + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.get(destination).get()); + assertTrue(executionException.getCause() instanceof RuntimeException); + assertEquals(0, client.setbit(key1, 0, 1).get()); + assertEquals(1L, client.bitop(BitwiseOperation.NOT, destination, new String[] {key1}).get()); + assertEquals("\u001e", client.get(destination).get()); + + // Returns null when all keys hold empty strings + assertEquals(0L, client.bitop(BitwiseOperation.AND, destination, emptyKeys).get()); + assertEquals(null, client.get(destination).get()); + assertEquals(0L, client.bitop(BitwiseOperation.OR, destination, emptyKeys).get()); + assertEquals(null, client.get(destination).get()); + assertEquals(0L, client.bitop(BitwiseOperation.XOR, destination, emptyKeys).get()); + assertEquals(null, client.get(destination).get()); + assertEquals( + 0L, client.bitop(BitwiseOperation.NOT, destination, new String[] {emptyKey1}).get()); + assertEquals(null, client.get(destination).get()); + + // Exception thrown due to the key holding a value with the wrong type + assertEquals(1, client.sadd(emptyKey1, new String[] {value1}).get()); + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitop(BitwiseOperation.AND, destination, new String[] {emptyKey1}).get()); + + // Source keys is an empty list + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitop(BitwiseOperation.OR, destination, new String[] {}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // NOT with more than one source key + executionException = + assertThrows( + ExecutionException.class, + () -> client.bitop(BitwiseOperation.NOT, destination, new String[] {key1, key2}).get()); + assertTrue(executionException.getCause() instanceof RequestException); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index e170b598f3..19a711bf61 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -20,6 +20,7 @@ import glide.api.models.commands.WeightAggregateOptions.Aggregate; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.bitmap.BitmapIndexType; +import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.geospatial.GeoUnit; import glide.api.models.commands.geospatial.GeospatialData; import glide.api.models.commands.stream.StreamAddOptions; @@ -557,6 +558,8 @@ private static Object[] scriptingAndFunctionsCommands(BaseTransaction transac private static Object[] bitmapCommands(BaseTransaction transaction) { String key1 = "{bitmapKey}-1" + UUID.randomUUID(); String key2 = "{bitmapKey}-2" + UUID.randomUUID(); + String key3 = "{bitmapKey}-3" + UUID.randomUUID(); + String key4 = "{bitmapKey}-4" + UUID.randomUUID(); transaction .set(key1, "foobar") @@ -567,7 +570,10 @@ private static Object[] bitmapCommands(BaseTransaction transaction) { .getbit(key1, 1) .bitpos(key1, 1) .bitpos(key1, 1, 3) - .bitpos(key1, 1, 3, 5); + .bitpos(key1, 1, 3, 5) + .set(key3, "abcdef") + .bitop(BitwiseOperation.AND, key4, new String[] {key1, key3}) + .get(key4); if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { transaction @@ -586,6 +592,9 @@ private static Object[] bitmapCommands(BaseTransaction transaction) { 1L, // bitpos(key, 1) 25L, // bitpos(key, 1, 3) 25L, // bitpos(key, 1, 3, 5) + OK, // set(key3, "abcdef") + 6L, // bitop(BitwiseOperation.AND, key4, new String[] {key1, key3}) + "`bc`ab", // get(key4) }; if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 0685a39128..b8ac7909c4 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -38,6 +38,7 @@ import glide.api.models.commands.InfoOptions; import glide.api.models.commands.RangeOptions.RangeByIndex; import glide.api.models.commands.WeightAggregateOptions.KeyArray; +import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.Route; @@ -691,7 +692,11 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { Arguments.of( "zmpop", "7.0.0", clusterClient.zmpop(new String[] {"abc", "zxy", "lkn"}, MAX)), Arguments.of( - "bzmpop", "7.0.0", clusterClient.bzmpop(new String[] {"abc", "zxy", "lkn"}, MAX, .1))); + "bzmpop", "7.0.0", clusterClient.bzmpop(new String[] {"abc", "zxy", "lkn"}, MAX, .1)), + Arguments.of( + "bitop", + null, + clusterClient.bitop(BitwiseOperation.OR, "abc", new String[] {"zxy", "lkn"}))); } @SneakyThrows