From bb6101c5fca1bdcf1710b656ba59996f6c98dfe4 Mon Sep 17 00:00:00 2001 From: tjzhang-BQ <111323543+tjzhang-BQ@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:04:18 -0700 Subject: [PATCH] Java: Adding command `LCS` (No IDX option) (#351) * Java: Adding command LCS (default & LEN option) --- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + .../src/main/java/glide/api/BaseClient.java | 13 ++++ .../api/commands/StringBaseCommands.java | 45 +++++++++++ .../glide/api/models/BaseTransaction.java | 37 +++++++++ .../test/java/glide/api/RedisClientTest.java | 52 +++++++++++++ .../glide/api/models/TransactionTests.java | 7 ++ .../test/java/glide/SharedCommandTests.java | 60 +++++++++++++++ .../java/glide/TransactionTestUtilities.java | 76 +++++++++++++------ .../test/java/glide/cluster/CommandTests.java | 4 +- 10 files changed, 275 insertions(+), 23 deletions(-) diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 1b6bceecfe..e633efefb5 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -214,6 +214,7 @@ enum RequestType { XRevRange = 176; Copy = 178; MSetNX = 179; + LCS = 181; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 7ae5b39293..216991da39 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -184,6 +184,7 @@ pub enum RequestType { XRevRange = 176, Copy = 178, MSetNX = 179, + LCS = 181, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -371,6 +372,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::Sort => RequestType::Sort, ProtobufRequestType::XRevRange => RequestType::XRevRange, ProtobufRequestType::MSetNX => RequestType::MSetNX, + ProtobufRequestType::LCS => RequestType::LCS, } } } @@ -554,6 +556,7 @@ impl RequestType { RequestType::Sort => Some(cmd("SORT")), RequestType::XRevRange => Some(cmd("XREVRANGE")), RequestType::MSetNX => Some(cmd("MSETNX")), + RequestType::LCS => Some(cmd("LCS")), } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 4d5f0e88c2..18a4b2aea7 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -60,6 +60,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.Incr; import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; +import static redis_request.RedisRequestOuterClass.RequestType.LCS; import static redis_request.RedisRequestOuterClass.RequestType.LIndex; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; @@ -1804,4 +1805,16 @@ public CompletableFuture msetnx(@NonNull Map keyValueMa String[] args = convertMapToKeyValueStringArray(keyValueMap); return commandManager.submitNewCommand(MSetNX, args, this::handleBooleanResponse); } + + @Override + public CompletableFuture lcs(@NonNull String key1, @NonNull String key2) { + String[] arguments = new String[] {key1, key2}; + return commandManager.submitNewCommand(LCS, arguments, this::handleStringResponse); + } + + @Override + public CompletableFuture lcsLen(@NonNull String key1, @NonNull String key2) { + String[] arguments = new String[] {key1, key2, LEN_REDIS_API}; + return commandManager.submitNewCommand(LCS, arguments, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java index 23496355ca..b38e21c573 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -15,6 +15,9 @@ */ public interface StringBaseCommands { + /** Redis API keyword used to indicate that the length of the lcs should be returned. */ + public static final String LEN_REDIS_API = "LEN"; + /** * Gets the value associated with the given key, or null if no such * value exists. @@ -333,4 +336,46 @@ public interface StringBaseCommands { * } */ CompletableFuture append(String key, String value); + + /** + * Returns the longest common subsequence between strings stored at key1 and + * key2. + * + * @since Redis 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return A String containing the longest common subsequence between the 2 strings. + * An empty String is returned if the keys do not exist or have no common + * subsequences. + * @example + *
{@code
+     * // testKey1 = abcd, testKey2 = axcd
+     * String result = client.lcs("testKey1", "testKey2").get();
+     * assert result.equals("acd");
+     * }
+ */ + CompletableFuture lcs(String key1, String key2); + + /** + * Returns the length of the longest common subsequence between strings stored at key1 + * and key2. + * + * @since Redis 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return The length of the longest common subsequence between the 2 strings. + * @example + *
{@code
+     * // testKey1 = abcd, testKey2 = axcd
+     * Long result = client.lcs("testKey1", "testKey2").get();
+     * assert result.equals(3L);
+     * }
+ */ + CompletableFuture lcsLen(String key1, String key2); } 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 4f4cebf27e..62a3128c47 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -10,6 +10,7 @@ import static glide.api.commands.SortedSetBaseCommands.LIMIT_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; +import static glide.api.commands.StringBaseCommands.LEN_REDIS_API; import static glide.api.models.commands.RangeOptions.createZRangeArgs; import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; import static glide.api.models.commands.function.FunctionListOptions.LIBRARY_NAME_REDIS_API; @@ -81,6 +82,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LCS; import static redis_request.RedisRequestOuterClass.RequestType.LIndex; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; @@ -4327,6 +4329,41 @@ public T functionDelete(@NonNull String libName) { return getThis(); } + /** + * Returns the longest common subsequence between strings stored at key1 and + * key2. + * + * @since Redis 7.0 and above. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return Command Response - A String containing the longest common subsequence + * between the 2 strings. An empty String is returned if the keys do not exist or + * have no common subsequences. + */ + public T lcs(@NonNull String key1, @NonNull String key2) { + protobufTransaction.addCommands(buildCommand(LCS, buildArgs(key1, key2))); + return getThis(); + } + + /** + * Returns the length of the longest common subsequence between strings stored at key1 + * and key2. + * + * @since Redis 7.0 and above. + * @apiNote When in cluster mode, key1 and key2 must map to the same + * hash slot. + * @see valkey.io for details. + * @param key1 The key that stores the first string. + * @param key2 The key that stores the second string. + * @return Command Response - The length of the longest common subsequence between the 2 strings. + */ + public T lcsLen(@NonNull String key1, @NonNull String key2) { + ArgsArray args = buildArgs(key1, key2, LEN_REDIS_API); + protobufTransaction.addCommands(buildCommand(LCS, args)); + 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/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 79c95d644e..e130235677 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -11,6 +11,7 @@ import static glide.api.commands.SortedSetBaseCommands.LIMIT_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; +import static glide.api.commands.StringBaseCommands.LEN_REDIS_API; import static glide.api.models.commands.FlushMode.ASYNC; import static glide.api.models.commands.FlushMode.SYNC; import static glide.api.models.commands.LInsertOptions.InsertPosition.BEFORE; @@ -113,6 +114,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LCS; import static redis_request.RedisRequestOuterClass.RequestType.LIndex; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; @@ -6015,4 +6017,54 @@ public void copy_with_destinationDB_returns_success() { assertEquals(testResponse, response); assertEquals(value, payload); } + + @SneakyThrows + @Test + public void lcs() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = new String[] {key1, key2}; + String value = "foo"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lcs(key1, key2); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void lcs_with_len_option() { + // setup + String key1 = "testKey1"; + String key2 = "testKey2"; + String[] arguments = new String[] {key1, key2, LEN_REDIS_API}; + Long value = 3L; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LCS), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lcsLen(key1, key2); + Long 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 a5564a332a..426569ed69 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -94,6 +94,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LCS; import static redis_request.RedisRequestOuterClass.RequestType.LIndex; import static redis_request.RedisRequestOuterClass.RequestType.LInsert; import static redis_request.RedisRequestOuterClass.RequestType.LLen; @@ -979,6 +980,12 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.copy("key1", "key2", true); results.add(Pair.of(Copy, buildArgs("key1", "key2", REPLACE_REDIS_API))); + transaction.lcs("key1", "key2"); + results.add(Pair.of(LCS, buildArgs("key1", "key2"))); + + transaction.lcsLen("key1", "key2"); + results.add(Pair.of(LCS, buildArgs("key1", "key2", "LEN"))); + 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 323cee1f78..0a9e75d1d7 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -5157,4 +5157,64 @@ public void msetnx(BaseClient client) { assertFalse(client.msetnx(keyValueMap2).get()); assertNull(client.get(key3).get()); } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lcs(BaseClient client) { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7.0.0"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String nonStringKey = "{key}-4" + UUID.randomUUID(); + + // keys does not exist or is empty + assertEquals("", client.lcs(key1, key2).get()); + + // setting string values + client.set(key1, "abcd"); + client.set(key2, "bcde"); + client.set(key3, "wxyz"); + + // getting the lcs + assertEquals("", client.lcs(key1, key3).get()); + assertEquals("bcd", client.lcs(key1, key2).get()); + + // non set keys are used + client.sadd(nonStringKey, new String[] {"setmember"}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.lcs(nonStringKey, key1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void lcs_with_len_option(BaseClient client) { + assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7.0.0"); + // setup + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String nonStringKey = "{key}-4" + UUID.randomUUID(); + + // keys does not exist or is empty + assertEquals(0, client.lcsLen(key1, key2).get()); + + // setting string values + client.set(key1, "abcd"); + client.set(key2, "bcde"); + client.set(key3, "wxyz"); + + // getting the lcs + assertEquals(0, client.lcsLen(key1, key3).get()); + assertEquals(3, client.lcsLen(key1, key2).get()); + + // non set keys are used + client.sadd(nonStringKey, new String[] {"setmember"}).get(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.lcs(nonStringKey, key1).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 ac754086df..9ed11d12aa 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -201,6 +201,9 @@ private static Object[] stringCommands(BaseTransaction transaction) { String stringKey3 = "{StringKey}-3-" + UUID.randomUUID(); String stringKey4 = "{StringKey}-4-" + UUID.randomUUID(); String stringKey5 = "{StringKey}-5-" + UUID.randomUUID(); + String stringKey6 = "{StringKey}-6-" + UUID.randomUUID(); + String stringKey7 = "{StringKey}-7-" + UUID.randomUUID(); + String stringKey8 = "{StringKey}-8-" + UUID.randomUUID(); transaction .set(stringKey1, value1) @@ -224,28 +227,57 @@ private static Object[] stringCommands(BaseTransaction transaction) { .msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) .mget(new String[] {stringKey4, stringKey5}); - return new Object[] { - OK, // set(stringKey1, value1) - value1, // get(stringKey1) - value1, // getdel(stringKey1) - null, // set(stringKey2, value2, returnOldValue(true)) - (long) value1.length(), // strlen(key2) - Long.valueOf(value2.length() * 2), // append(key2, value2) - OK, // mset(Map.of(stringKey1, value2, stringKey2, value1)) - new String[] {value2, value1}, // mget(new String[] {stringKey1, stringKey2}) - 1L, // incr(stringKey3) - 3L, // incrBy(stringKey3, 2) - 2L, // decr(stringKey3) - 0L, // decrBy(stringKey3, 2) - 0.5, // incrByFloat(stringKey3, 0.5) - 5L, // setrange(stringKey3, 0, "GLIDE") - "GLIDE", // getrange(stringKey3, 0, 5) - true, // msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) - new String[] {"foo", "bar"}, // mget({stringKey4, stringKey5}) - 1L, // del(stringKey5) - false, // msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) - new String[] {"foo", null}, // mget({stringKey4, stringKey5}) - }; + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + transaction + .set(stringKey6, "abcd") + .set(stringKey7, "bcde") + .set(stringKey8, "wxyz") + .lcs(stringKey6, stringKey7) + .lcs(stringKey6, stringKey8) + .lcsLen(stringKey6, stringKey7) + .lcsLen(stringKey6, stringKey8); + } + + var expectedResults = + new Object[] { + OK, // set(stringKey1, value1) + value1, // get(stringKey1) + value1, // getdel(stringKey1) + null, // set(stringKey2, value2, returnOldValue(true)) + (long) value1.length(), // strlen(key2) + Long.valueOf(value2.length() * 2), // append(key2, value2) + OK, // mset(Map.of(stringKey1, value2, stringKey2, value1)) + new String[] {value2, value1}, // mget(new String[] {stringKey1, stringKey2}) + 1L, // incr(stringKey3) + 3L, // incrBy(stringKey3, 2) + 2L, // decr(stringKey3) + 0L, // decrBy(stringKey3, 2) + 0.5, // incrByFloat(stringKey3, 0.5) + 5L, // setrange(stringKey3, 0, "GLIDE") + "GLIDE", // getrange(stringKey3, 0, 5) + true, // msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) + new String[] {"foo", "bar"}, // mget({stringKey4, stringKey5}) + 1L, // del(stringKey5) + false, // msetnx(Map.of(stringKey4, "foo", stringKey5, "bar")) + new String[] {"foo", null}, // mget({stringKey4, stringKey5}) + }; + + if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { + expectedResults = + concatenateArrays( + expectedResults, + new Object[] { + OK, // set(stringKey6, "abcd") + OK, // set(stringKey6, "bcde") + OK, // set(stringKey6, "wxyz") + "bcd", // lcs(stringKey6, stringKey7) + "", // lcs(stringKey6, stringKey8) + 3L, // lcsLEN(stringKey6, stringKey7) + 0L, // lcsLEN(stringKey6, stringKey8) + }); + } + + return expectedResults; } private static Object[] hashCommands(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 5df004426b..1ef67f178d 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -758,7 +758,9 @@ public static Stream callCrossSlotCommandsWhichShouldFail() { Arguments.of( "xread", null, clusterClient.xread(Map.of("abc", "stream1", "zxy", "stream2"))), Arguments.of("copy", "6.2.0", clusterClient.copy("abc", "def", true)), - Arguments.of("msetnx", null, clusterClient.msetnx(Map.of("abc", "def", "ghi", "jkl")))); + Arguments.of("msetnx", null, clusterClient.msetnx(Map.of("abc", "def", "ghi", "jkl"))), + Arguments.of("lcs", "7.0.0", clusterClient.lcs("abc", "def")), + Arguments.of("lcsLEN", "7.0.0", clusterClient.lcsLen("abc", "def"))); } @SneakyThrows