diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 02652ff6f5..c68e3a0dd1 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -16,9 +16,11 @@ pub(crate) enum ExpectedReturnType { DoubleOrNull, ZRankReturnType, JsonToggleReturnType, + ArrayOfStrings, ArrayOfBools, ArrayOfDoubleOrNull, Lolwut, + ArrayOfStringAndArrays, ArrayOfArraysOfDoubleOrNull, ArrayOfPairs, ArrayOfMemberScorePairs, @@ -176,6 +178,14 @@ pub(crate) fn convert_to_expected_type( ) .into()), }, + ExpectedReturnType::ArrayOfStrings => match value { + Value::Array(array) => convert_array_elements(array, ExpectedReturnType::BulkString), + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to an array of bulk strings", + ) + .into()), + }, ExpectedReturnType::ArrayOfDoubleOrNull => match value { Value::Array(array) => convert_array_elements(array, ExpectedReturnType::DoubleOrNull), _ => Err(( @@ -324,6 +334,29 @@ pub(crate) fn convert_to_expected_type( // RESP2 returns scores as strings, but we want scores as type double. convert_to_array_of_pairs(value, Some(ExpectedReturnType::Double)) } + // Used by LMPOP and BLMPOP + // The server response can be an array or null + // + // Example: + // let input = ["key", "val1", "val2"] + // let expected =("key", vec!["val1", "val2"]) + ExpectedReturnType::ArrayOfStringAndArrays => match value { + Value::Nil => Ok(value), + Value::Array(array) if array.len() == 2 && matches!(array[1], Value::Array(_)) => { + // convert the array to a map of string to string-array + let map = convert_array_to_map( + array, + Some(ExpectedReturnType::BulkString), + Some(ExpectedReturnType::ArrayOfStrings), + )?; + Ok(map) + } + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to a pair of String/String-Array return type", + ) + .into()), + }, // Used by BZPOPMIN/BZPOPMAX, which return an array consisting of the key of the sorted set that was popped, the popped member, and its score. // RESP2 returns the score as a string, but RESP3 returns the score as a double. Here we convert string scores into type double. ExpectedReturnType::KeyWithMemberAndScore => match value { @@ -508,6 +541,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { b"BZMPOP" | b"ZMPOP" => Some(ExpectedReturnType::ZMPopReturnType), b"JSON.TOGGLE" => Some(ExpectedReturnType::JsonToggleReturnType), b"GEOPOS" => Some(ExpectedReturnType::ArrayOfArraysOfDoubleOrNull), + b"LMPOP" => Some(ExpectedReturnType::ArrayOfStringAndArrays), b"HRANDFIELD" => cmd .position(b"WITHVALUES") .map(|_| ExpectedReturnType::ArrayOfPairs), @@ -752,6 +786,28 @@ mod tests { assert_eq!(expected_response, converted_flat_array); } + #[test] + fn convert_to_array_of_string_and_array_return_type() { + assert!(matches!( + expected_type_for_cmd(redis::cmd("LMPOP").arg("1").arg("key").arg("LEFT")), + Some(ExpectedReturnType::ArrayOfStringAndArrays) + )); + + // testing value conversion + let flat_array = Value::Array(vec![ + Value::BulkString(b"1".to_vec()), + Value::Array(vec![Value::BulkString(b"one".to_vec())]), + ]); + let expected_response = Value::Map(vec![( + Value::BulkString("1".into()), + Value::Array(vec![Value::BulkString(b"one".to_vec())]), + )]); + let converted_flat_array = + convert_to_expected_type(flat_array, Some(ExpectedReturnType::ArrayOfStringAndArrays)) + .unwrap(); + assert_eq!(expected_response, converted_flat_array); + } + #[test] fn convert_zadd_only_if_incr_is_included() { assert!(matches!( diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 866c0196da..c256b64e70 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -189,6 +189,7 @@ enum RequestType { BitPos = 147; BitOp = 148; FunctionLoad = 150; + LMPop = 155; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 92fad77c5c..803eb42e4c 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -159,6 +159,7 @@ pub enum RequestType { BitPos = 147, BitOp = 148, FunctionLoad = 150, + LMPop = 155, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -314,6 +315,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::BitCount => RequestType::BitCount, ProtobufRequestType::BZMPop => RequestType::BZMPop, ProtobufRequestType::SetBit => RequestType::SetBit, + ProtobufRequestType::LMPop => RequestType::LMPop, ProtobufRequestType::ZInterCard => RequestType::ZInterCard, ProtobufRequestType::ZMPop => RequestType::ZMPop, ProtobufRequestType::GetBit => RequestType::GetBit, @@ -471,6 +473,7 @@ impl RequestType { RequestType::ZRandMember => Some(cmd("ZRANDMEMBER")), RequestType::BitCount => Some(cmd("BITCOUNT")), RequestType::BZMPop => Some(cmd("BZMPOP")), + RequestType::LMPop => Some(cmd("LMPOP")), RequestType::SetBit => Some(cmd("SETBIT")), RequestType::ZInterCard => Some(cmd("ZINTERCARD")), RequestType::ZMPop => Some(cmd("ZMPOP")), diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index f8333bc711..9c6c5ae5fd 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -4,6 +4,7 @@ import static glide.ffi.resolvers.SocketListenerResolver.getSocket; import static glide.utils.ArrayTransformUtils.castArray; import static glide.utils.ArrayTransformUtils.castArrayofArrays; +import static glide.utils.ArrayTransformUtils.castMapOfArrays; import static glide.utils.ArrayTransformUtils.concatenateArrays; import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArray; @@ -49,6 +50,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.LIndex; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; +import static redis_request.RedisRequestOuterClass.RequestType.LMPop; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; import static redis_request.RedisRequestOuterClass.RequestType.LPushX; @@ -134,6 +136,7 @@ import glide.api.models.Script; import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.LInsertOptions.InsertPosition; +import glide.api.models.commands.PopDirection; import glide.api.models.commands.RangeOptions; import glide.api.models.commands.RangeOptions.LexRange; import glide.api.models.commands.RangeOptions.RangeQuery; @@ -340,6 +343,16 @@ protected Map handleMapResponse(Response response) throws RedisEx return handleRedisResponse(Map.class, false, response); } + /** + * @param response A Protobuf response + * @return A map of String to V or null + * @param Value type. + */ + @SuppressWarnings("unchecked") // raw Map cast to Map + protected Map handleMapOrNullResponse(Response response) throws RedisException { + return handleRedisResponse(Map.class, true, response); + } + @SuppressWarnings("unchecked") // raw Set cast to Set protected Set handleSetResponse(Response response) throws RedisException { return handleRedisResponse(Set.class, false, response); @@ -1454,4 +1467,30 @@ public CompletableFuture bitop( concatenateArrays(new String[] {bitwiseOperation.toString(), destination}, keys); return commandManager.submitNewCommand(BitOp, arguments, this::handleLongResponse); } + + @Override + public CompletableFuture> lmpop( + @NonNull String[] keys, @NonNull PopDirection direction, long count) { + String[] arguments = + concatenateArrays( + new String[] {Long.toString(keys.length)}, + keys, + new String[] {direction.toString(), COUNT_FOR_LIST_REDIS_API, Long.toString(count)}); + return commandManager.submitNewCommand( + LMPop, + arguments, + response -> castMapOfArrays(handleMapOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture> lmpop( + @NonNull String[] keys, @NonNull PopDirection direction) { + String[] arguments = + concatenateArrays( + new String[] {Long.toString(keys.length)}, keys, new String[] {direction.toString()}); + return commandManager.submitNewCommand( + LMPop, + arguments, + response -> castMapOfArrays(handleMapOrNullResponse(response), String.class)); + } } 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 3c0de6a606..66bd5c39ba 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -2,6 +2,8 @@ package glide.api.commands; import glide.api.models.commands.LInsertOptions.InsertPosition; +import glide.api.models.commands.PopDirection; +import java.util.Map; import java.util.concurrent.CompletableFuture; /** @@ -11,6 +13,8 @@ * @see List Commands */ public interface ListBaseCommands { + /** Redis API keyword used to extract specific count of members from a sorted set. */ + String COUNT_FOR_LIST_REDIS_API = "COUNT"; /** * Inserts all the specified values at the head of the list stored at key. @@ -369,4 +373,46 @@ CompletableFuture linsert( * } */ CompletableFuture lpushx(String key, String[] elements); + + /** + * Pops one or more elements from the first non-empty list from the provided keys + * . + * + * @since Redis 7.0 and above. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * PopDirection}. + * @param count The maximum number of popped elements. + * @return A Map of key name mapped array of popped elements. + * @example + *
{@code
+     * client.lpush("testKey", new String[] {"one", "two", "three"}).get();
+     * Map result = client.lmpop(new String[] {"testKey"}, PopDirection.LEFT, 1L).get();
+     * String[] resultValue = result.get("testKey");
+     * assertArrayEquals(new String[] {"three"}, resultValue);
+     * }
+ */ + CompletableFuture> lmpop(String[] keys, PopDirection direction, long count); + + /** + * Pops one element from the first non-empty list from the provided keys. + * + * @since Redis 7.0 and above. + * @apiNote When in cluster mode, all keys must map to the same hash slot. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * PopDirection}. + * @return A Map of key name mapped array of the popped element. + * @example + *
{@code
+     * client.lpush("testKey", new String[] {"one", "two", "three"}).get();
+     * Map result = client.lmpop(new String[] {"testKey"}, PopDirection.LEFT).get();
+     * String[] resultValue = result.get("testKey");
+     * assertArrayEquals(new String[] {"three"}, resultValue);
+     * }
+ */ + CompletableFuture> lmpop(String[] keys, PopDirection direction); } 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 227e90ec7c..1aa32c8d08 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -2,6 +2,7 @@ package glide.api.models; import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API; +import static glide.api.commands.ListBaseCommands.COUNT_FOR_LIST_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; @@ -64,6 +65,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.LIndex; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; +import static redis_request.RedisRequestOuterClass.RequestType.LMPop; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; import static redis_request.RedisRequestOuterClass.RequestType.LPushX; @@ -145,6 +147,7 @@ import glide.api.models.commands.InfoOptions; import glide.api.models.commands.InfoOptions.Section; import glide.api.models.commands.LInsertOptions.InsertPosition; +import glide.api.models.commands.PopDirection; import glide.api.models.commands.RangeOptions; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; @@ -3552,6 +3555,54 @@ public T bitop( return getThis(); } + /** + * Pops one or more elements from the first non-empty list from the provided keys + * . + * + * @since Redis 7.0 and above. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * PopDirection}. + * @param count The maximum number of popped elements. + * @return Command Response - A Map of key name mapped arrays of popped + * elements. + */ + public T lmpop(@NonNull String[] keys, @NonNull PopDirection direction, @NonNull Long count) { + ArgsArray commandArgs = + buildArgs( + concatenateArrays( + new String[] {Long.toString(keys.length)}, + keys, + new String[] { + direction.toString(), COUNT_FOR_LIST_REDIS_API, Long.toString(count) + })); + protobufTransaction.addCommands(buildCommand(LMPop, commandArgs)); + return getThis(); + } + + /** + * Pops one element from the first non-empty list from the provided keys. + * + * @since Redis 7.0 and above. + * @see valkey.io for details. + * @param keys An array of keys to lists. + * @param direction The direction based on which elements are popped from - see {@link + * PopDirection}. + * @return Command Response - A Map of key name mapped array of the + * popped element. + */ + public T lmpop(@NonNull String[] keys, @NonNull PopDirection direction) { + ArgsArray commandArgs = + buildArgs( + concatenateArrays( + new String[] {Long.toString(keys.length)}, + keys, + new String[] {direction.toString()})); + protobufTransaction.addCommands(buildCommand(LMPop, 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/PopDirection.java b/java/client/src/main/java/glide/api/models/commands/PopDirection.java new file mode 100644 index 0000000000..bdf6e02011 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/PopDirection.java @@ -0,0 +1,14 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.ListBaseCommands; + +/** + * Enumeration representing element pop direction for the {@link ListBaseCommands#lmpop} command. + */ +public enum PopDirection { + /** Represents the option that elements should be popped from the left side of a list. */ + LEFT, + /** Represents the option that elements should be popped from the right side of a list. */ + RIGHT +} diff --git a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java index a5cfaba908..372cc4f336 100644 --- a/java/client/src/main/java/glide/utils/ArrayTransformUtils.java +++ b/java/client/src/main/java/glide/utils/ArrayTransformUtils.java @@ -109,6 +109,9 @@ public static U[][] castArrayofArrays(T[] outerObjectArr, Class @SuppressWarnings("unchecked") public static Map castMapOfArrays( Map mapOfArrays, Class clazz) { + if (mapOfArrays == null) { + return null; + } return mapOfArrays.entrySet().stream() .collect(Collectors.toMap(k -> k.getKey(), e -> castArray(e.getValue(), clazz))); } diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index f68b2195cf..e5b8d1ee0b 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -3,6 +3,7 @@ import static glide.api.BaseClient.OK; import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API; +import static glide.api.commands.ListBaseCommands.COUNT_FOR_LIST_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; @@ -85,6 +86,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.LIndex; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; +import static redis_request.RedisRequestOuterClass.RequestType.LMPop; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; import static redis_request.RedisRequestOuterClass.RequestType.LPushX; @@ -166,6 +168,7 @@ import glide.api.models.commands.ConditionalChange; import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.PopDirection; import glide.api.models.commands.RangeOptions; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; @@ -4901,4 +4904,62 @@ public void bitop_returns_success() { assertEquals(testResponse, response); assertEquals(result, payload); } + + @SneakyThrows + @Test + public void lmpop_returns_success() { + // setup + String key = "testKey"; + String key2 = "testKey2"; + String[] keys = {key, key2}; + PopDirection popDirection = PopDirection.LEFT; + String[] arguments = new String[] {"2", key, key2, popDirection.toString()}; + Map value = Map.of(key, new String[] {"five"}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lmpop(keys, popDirection); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lmpop_with_count_returns_success() { + // setup + String key = "testKey"; + String key2 = "testKey2"; + String[] keys = {key, key2}; + PopDirection popDirection = PopDirection.LEFT; + long count = 1L; + String[] arguments = + new String[] { + "2", key, key2, popDirection.toString(), COUNT_FOR_LIST_REDIS_API, Long.toString(count) + }; + Map value = Map.of(key, new String[] {"five"}); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(LMPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.lmpop(keys, popDirection, count); + Map payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, 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 828339d91e..42598f8b15 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -75,6 +75,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.LIndex; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; +import static redis_request.RedisRequestOuterClass.RequestType.LMPop; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; import static redis_request.RedisRequestOuterClass.RequestType.LPushX; @@ -153,6 +154,7 @@ import glide.api.models.commands.ConditionalChange; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.PopDirection; import glide.api.models.commands.RangeOptions; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; @@ -828,6 +830,11 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.bitop(BitwiseOperation.AND, "destination", new String[] {"key"}); results.add(Pair.of(BitOp, buildArgs(BitwiseOperation.AND.toString(), "destination", "key"))); + transaction.lmpop(new String[] {"key"}, PopDirection.LEFT); + results.add(Pair.of(LMPop, buildArgs("1", "key", "LEFT"))); + transaction.lmpop(new String[] {"key"}, PopDirection.LEFT, 1L); + results.add(Pair.of(LMPop, buildArgs("1", "key", "LEFT", "COUNT", "1"))); + 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 ca2f1dbcea..9c36396bff 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -4,6 +4,7 @@ import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestConfiguration.STANDALONE_PORTS; +import static glide.TestUtilities.assertDeepEquals; import static glide.TestUtilities.commonClientConfig; import static glide.TestUtilities.commonClusterClientConfig; import static glide.api.BaseClient.OK; @@ -32,6 +33,7 @@ import glide.api.models.Script; import glide.api.models.commands.ConditionalChange; import glide.api.models.commands.ExpireOptions; +import glide.api.models.commands.PopDirection; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; import glide.api.models.commands.RangeOptions.LexBoundary; @@ -4061,4 +4063,46 @@ public void bitop(BaseClient client) { () -> client.bitop(BitwiseOperation.NOT, destination, new String[] {key1, key2}).get()); assertTrue(executionException.getCause() instanceof RequestException); } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lmpop(BaseClient client) { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String nonListKey = "{key}-3" + UUID.randomUUID(); + String[] singleKeyArray = {key1}; + String[] multiKeyArray = {key2, key1}; + long count = 1L; + Long arraySize = 5L; + String[] lpushArgs = {"one", "two", "three", "four", "five"}; + Map expected = Map.of(key1, new String[] {"five"}); + Map expected2 = Map.of(key2, new String[] {"one", "two"}); + + // nothing to be popped + assertNull(client.lmpop(singleKeyArray, PopDirection.LEFT).get()); + assertNull(client.lmpop(singleKeyArray, PopDirection.LEFT, count).get()); + + // pushing to the arrays to be popped + assertEquals(arraySize, client.lpush(key1, lpushArgs).get()); + assertEquals(arraySize, client.lpush(key2, lpushArgs).get()); + + // assert correct result from popping + Map result = client.lmpop(singleKeyArray, PopDirection.LEFT).get(); + assertDeepEquals(result, expected); + + // assert popping multiple elements from the right + Map result2 = client.lmpop(multiKeyArray, PopDirection.RIGHT, 2L).get(); + assertDeepEquals(result2, expected2); + + // key exists but is not a list type key + assertEquals(OK, client.set(nonListKey, "lmpop").get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> client.lmpop(new String[] {nonListKey}, PopDirection.LEFT).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 19a711bf61..cde8a3a229 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -11,6 +11,7 @@ import glide.api.models.BaseTransaction; import glide.api.models.commands.ExpireOptions; +import glide.api.models.commands.PopDirection; import glide.api.models.commands.RangeOptions.InfLexBound; import glide.api.models.commands.RangeOptions.InfScoreBound; import glide.api.models.commands.RangeOptions.LexBoundary; @@ -242,6 +243,7 @@ private static Object[] listCommands(BaseTransaction transaction) { String listKey1 = "{ListKey}-1-" + UUID.randomUUID(); String listKey2 = "{ListKey}-2-" + UUID.randomUUID(); String listKey3 = "{ListKey}-3-" + UUID.randomUUID(); + String listKey4 = "{ListKey}-4-" + UUID.randomUUID(); transaction .lpush(listKey1, new String[] {value1, value1, value2, value3, value3}) @@ -262,25 +264,45 @@ private static Object[] listCommands(BaseTransaction transaction) { .blpop(new String[] {listKey3}, 0.01) .brpop(new String[] {listKey3}, 0.01); - return new Object[] { - 5L, // lpush(listKey1, new String[] {value1, value1, value2, value3, value3}) - 5L, // llen(listKey1) - value3, // lindex(key5, 0) - 1L, // lrem(listKey1, 1, value1) - OK, // ltrim(listKey1, 1, -1) - new String[] {value3, value2}, // lrange(listKey1, 0, -2) - value3, // lpop(listKey1) - new String[] {value2, value1}, // lpopCount(listKey1, 2) - 3L, // rpush(listKey2, new String[] {value1, value2, value2}) - value2, // rpop(listKey2) - new String[] {value2, value1}, // rpopCount(listKey2, 2) - 0L, // rpushx(listKey3, new String[] { "_" }) - 0L, // lpushx(listKey3, new String[] { "_" }) - 3L, // lpush(listKey3, new String[] { value1, value2, value3}) - 4L, // linsert(listKey3, AFTER, value2, value2) - new String[] {listKey3, value3}, // blpop(new String[] { listKey3 }, 0.01) - new String[] {listKey3, value1}, // brpop(new String[] { listKey3 }, 0.01) - }; + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction + .lpush(listKey4, new String[] {value1, value2, value3}) + .lmpop(new String[] {listKey4}, PopDirection.LEFT) + .lmpop(new String[] {listKey4}, PopDirection.LEFT, 2L); + } // listKey4 is now empty + + var expectedResults = + new Object[] { + 5L, // lpush(listKey1, new String[] {value1, value1, value2, value3, value3}) + 5L, // llen(listKey1) + value3, // lindex(key5, 0) + 1L, // lrem(listKey1, 1, value1) + OK, // ltrim(listKey1, 1, -1) + new String[] {value3, value2}, // lrange(listKey1, 0, -2) + value3, // lpop(listKey1) + new String[] {value2, value1}, // lpopCount(listKey1, 2) + 3L, // rpush(listKey2, new String[] {value1, value2, value2}) + value2, // rpop(listKey2) + new String[] {value2, value1}, // rpopCount(listKey2, 2) + 0L, // rpushx(listKey3, new String[] { "_" }) + 0L, // lpushx(listKey3, new String[] { "_" }) + 3L, // lpush(listKey3, new String[] { value1, value2, value3}) + 4L, // linsert(listKey3, AFTER, value2, value2) + new String[] {listKey3, value3}, // blpop(new String[] { listKey3 }, 0.01) + new String[] {listKey3, value1}, // brpop(new String[] { listKey3 }, 0.01) + }; + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + return concatenateArrays( + expectedResults, + new Object[] { + 3L, // lpush(listKey4, {value1, value2, value3}) + Map.of(listKey4, new String[] {value3}), // lmpop({listKey4}, LEFT) + Map.of(listKey4, new String[] {value2, value1}), // lmpop({listKey4}, LEFT, 1L) + }); + } + + return expectedResults; } private static Object[] setCommands(BaseTransaction transaction) { diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index b8ac7909c4..ad623e317d 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -36,6 +36,7 @@ import glide.api.models.ClusterValue; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions; +import glide.api.models.commands.PopDirection; import glide.api.models.commands.RangeOptions.RangeByIndex; import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.bitmap.BitwiseOperation; @@ -693,6 +694,10 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { "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)), + Arguments.of( + "lmpop", + "7.0.0", + clusterClient.lmpop(new String[] {"abc", "def"}, PopDirection.LEFT, 1L)), Arguments.of( "bitop", null,