diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index a5acd8e2d2..eafb5bb01e 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -191,6 +191,7 @@ enum RequestType { HStrlen = 149; FunctionLoad = 150; FunctionList = 151; + FunctionDelete = 152; LMPop = 155; ExpireTime = 156; PExpireTime = 157; diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index ed86f72d51..57e3693c13 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -161,6 +161,7 @@ pub enum RequestType { HStrlen = 149, FunctionLoad = 150, FunctionList = 151, + FunctionDelete = 152, LMPop = 155, ExpireTime = 156, PExpireTime = 157, @@ -340,6 +341,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::ZInter => RequestType::ZInter, ProtobufRequestType::FunctionLoad => RequestType::FunctionLoad, ProtobufRequestType::FunctionList => RequestType::FunctionList, + ProtobufRequestType::FunctionDelete => RequestType::FunctionDelete, ProtobufRequestType::BitPos => RequestType::BitPos, ProtobufRequestType::BitOp => RequestType::BitOp, ProtobufRequestType::HStrlen => RequestType::HStrlen, @@ -516,6 +518,7 @@ impl RequestType { RequestType::ZInter => Some(cmd("ZINTER")), RequestType::FunctionLoad => Some(get_two_word_command("FUNCTION", "LOAD")), RequestType::FunctionList => Some(get_two_word_command("FUNCTION", "LIST")), + RequestType::FunctionDelete => Some(get_two_word_command("FUNCTION", "DELETE")), RequestType::BitPos => Some(cmd("BITPOS")), RequestType::BitOp => Some(cmd("BITOP")), RequestType::HStrlen => Some(cmd("HSTRLEN")), diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 8a77bbc9e6..94672003cd 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -16,6 +16,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; @@ -225,4 +226,10 @@ public CompletableFuture[]> functionList( : new String[] {LIBRARY_NAME_REDIS_API, libNamePattern}, response -> handleFunctionListResponse(handleArrayResponse(response))); } + + @Override + public CompletableFuture functionDelete(@NonNull String libName) { + return commandManager.submitNewCommand( + FunctionDelete, new String[] {libName}, this::handleStringResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index f758ecd8eb..b45f814c91 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -18,6 +18,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; @@ -494,4 +495,16 @@ public CompletableFuture[]>> functionList( route, response -> handleFunctionListResponse(response, route)); } + + @Override + public CompletableFuture functionDelete(@NonNull String libName) { + return commandManager.submitNewCommand( + FunctionDelete, new String[] {libName}, this::handleStringResponse); + } + + @Override + public CompletableFuture functionDelete(@NonNull String libName, @NonNull Route route) { + return commandManager.submitNewCommand( + FunctionDelete, new String[] {libName}, route, this::handleStringResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java index baa38ab057..a868a8d82e 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsClusterCommands.java @@ -167,4 +167,37 @@ CompletableFuture[]>> functionList( */ CompletableFuture[]>> functionList( String libNamePattern, boolean withCode, Route route); + + /** + * Deletes a library and all its functions.
+ * The command will be routed to all primary nodes. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param libName The library name to delete. + * @return OK. + * @example + *
{@code
+     * String response = client.functionDelete("myLib").get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionDelete(String libName); + + /** + * Deletes a library and all its functions. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param libName The library name to delete. + * @param route Specifies the routing configuration for the command. The client will route the + * command to the nodes defined by route. + * @return OK. + * @example + *
{@code
+     * String response = client.functionDelete("myLib", RANDOM).get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionDelete(String libName, Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java index baffdf2e21..1097fdadd5 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsCommands.java @@ -81,4 +81,19 @@ public interface ScriptingAndFunctionsCommands { * } */ CompletableFuture[]> functionList(String libNamePattern, boolean withCode); + + /** + * Deletes a library and all its functions. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param libName The library name to delete. + * @return OK. + * @example + *
{@code
+     * String response = client.functionDelete("myLib").get();
+     * assert response.equals("OK");
+     * }
+ */ + CompletableFuture functionDelete(String libName); } 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 f0e9653785..cfa46cd828 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -47,6 +47,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; @@ -4107,6 +4108,19 @@ public T bitfieldReadOnly( return getThis(); } + /** + * Deletes a library and all its functions. + * + * @since Redis 7.0 and above. + * @see redis.io for details. + * @param libName The library name to delete. + * @return Command Response - OK. + */ + public T functionDelete(@NonNull String libName) { + protobufTransaction.addCommands(buildCommand(FunctionDelete, buildArgs(libName))); + 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 008e7bd1dd..c96ca86e81 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -74,6 +74,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; @@ -4967,6 +4968,29 @@ public void functionList_with_pattern_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void functionDelete_returns_success() { + // setup + String libName = "GLIDE"; + String[] args = new String[] {libName}; + String value = OK; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDelete(libName); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void bitcount_returns_success() { @@ -5351,7 +5375,6 @@ public void lmove_returns_success() { ListDirection whereto = ListDirection.RIGHT; String[] arguments = new String[] {key1, key2, wherefrom.toString(), whereto.toString()}; String value = "one"; - CompletableFuture testResponse = new CompletableFuture<>(); testResponse.complete(value); diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 1a32dd7669..6e75de9f57 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -25,6 +25,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ConfigSet; import static redis_request.RedisRequestOuterClass.RequestType.Echo; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.Info; @@ -1270,4 +1271,50 @@ public void functionList_with_pattern_and_route_returns_success() { assertEquals(testResponse, response); assertEquals(value, payload.getSingleValue()); } + + @SneakyThrows + @Test + public void functionDelete_returns_success() { + // setup + String libName = "GLIDE"; + String[] args = new String[] {libName}; + String value = OK; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDelete(libName); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void functionDelete_with_route_returns_success() { + // setup + String libName = "GLIDE"; + String[] args = new String[] {libName}; + String value = OK; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(FunctionDelete), eq(args), eq(RANDOM), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.functionDelete(libName, RANDOM); + String 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 e887b2e06a..5d1b41988e 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -57,6 +57,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.ExpireTime; import static redis_request.RedisRequestOuterClass.RequestType.FlushAll; +import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete; import static redis_request.RedisRequestOuterClass.RequestType.FunctionList; import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad; import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd; @@ -935,6 +936,9 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.sintercard(new String[] {"key1", "key2"}, 1); results.add(Pair.of(SInterCard, buildArgs("2", "key1", "key2", "LIMIT", "1"))); + transaction.functionDelete("LIB"); + results.add(Pair.of(FunctionDelete, buildArgs("LIB"))); + 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 2064cc8087..1ab8235f72 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -4365,8 +4365,8 @@ public void blmpop_timeout_check(BaseClient client) { @MethodSource("getClients") public void lset(BaseClient client) { // setup - String key = "testKey"; - String nonExistingKey = "nonExisting"; + String key = UUID.randomUUID().toString(); + String nonExistingKey = UUID.randomUUID().toString(); long index = 0; long oobIndex = 10; long negativeIndex = -1; diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java index 3b77497fcd..34659599ae 100644 --- a/java/integTest/src/test/java/glide/TestUtilities.java +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -163,4 +163,16 @@ public static void checkFunctionListResponse( } assertTrue(hasLib); } + + /** Generate a dummy LUA library code. */ + public static String generateLuaLibCode(String libName, List funcNames) { + StringBuilder code = new StringBuilder("#!lua name=" + libName + "\n"); + for (var funcName : funcNames) { + code.append("redis.register_function('") + .append(funcName) + .append( + "', function(keys, args) return args[1] end)\n"); // function returns first argument + } + return code.toString(); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index b847d39635..5f9bdcbe9a 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -705,7 +705,7 @@ private static Object[] scriptingAndFunctionsCommands(BaseTransaction transac .functionLoad(code, true) .functionList("otherLib", false) .functionList("mylib1T", true) - .customCommand(new String[] {"function", "flush", "sync"}); + .functionDelete("mylib1T"); return new Object[] { OK, // customCommand("function", "flush", "sync") @@ -715,7 +715,7 @@ private static Object[] scriptingAndFunctionsCommands(BaseTransaction transac "mylib1T", // functionLoad(code, true) new Map[0], // functionList("otherLib", false) expectedLibData, // functionList("mylib1T", true) - OK, // customCommand("function", "flush", "sync") + OK, // functionDelete("mylib1T") }; } diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 4d2779b8bc..1428ad676e 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -4,6 +4,7 @@ import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestUtilities.checkFunctionListResponse; +import static glide.TestUtilities.generateLuaLibCode; import static glide.TestUtilities.getFirstEntryFromMultiValue; import static glide.TestUtilities.getValueFromInfo; import static glide.TestUtilities.parseInfoResponseToMap; @@ -785,7 +786,7 @@ public void flushall() { @SneakyThrows @ParameterizedTest(name = "functionLoad: singleNodeRoute = {0}") @ValueSource(booleans = {true, false}) - public void functionLoad_and_functionList(boolean singleNodeRoute) { + public void function_commands(boolean singleNodeRoute) { assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); // TODO use FUNCTION FLUSH @@ -798,12 +799,7 @@ public void functionLoad_and_functionList(boolean singleNodeRoute) { String libName = "mylib1c_" + singleNodeRoute; String funcName = "myfunc1c_" + singleNodeRoute; - String code = - "#!lua name=" - + libName - + " \n redis.register_function('" - + funcName - + "', function(keys, args) return args[1] end)"; // function returns first argument + String code = generateLuaLibCode(libName, List.of(funcName)); Route route = singleNodeRoute ? new SlotKeyRoute("1", PRIMARY) : ALL_PRIMARIES; assertEquals(libName, clusterClient.functionLoad(code, false, route).get()); @@ -857,11 +853,7 @@ public void functionLoad_and_functionList(boolean singleNodeRoute) { // re-load library with overwriting assertEquals(libName, clusterClient.functionLoad(code, true, route).get()); String newFuncName = "myfunc2c_" + singleNodeRoute; - String newCode = - code - + "\n redis.register_function('" - + newFuncName - + "', function(keys, args) return #args end)"; // function returns argument count + String newCode = generateLuaLibCode(libName, List.of(funcName, newFuncName)); assertEquals(libName, clusterClient.functionLoad(newCode, true, route).get()); @@ -880,6 +872,11 @@ public void functionLoad_and_functionList(boolean singleNodeRoute) { } } + // load new lib and delete it - first lib remains loaded + String anotherLib = generateLuaLibCode("anotherLib", List.of("anotherFunc")); + assertEquals("anotherLib", clusterClient.functionLoad(anotherLib, true, route).get()); + assertEquals(OK, clusterClient.functionDelete("anotherLib", route).get()); + response = clusterClient.functionList(true, route).get(); if (singleNodeRoute) { var flist = response.getSingleValue(); @@ -899,7 +896,7 @@ public void functionLoad_and_functionList(boolean singleNodeRoute) { @SneakyThrows @Test - public void functionLoad_and_functionList_without_route() { + public void function_commands() { assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); // TODO use FUNCTION FLUSH @@ -912,12 +909,7 @@ public void functionLoad_and_functionList_without_route() { String libName = "mylib1c"; String funcName = "myfunc1c"; - String code = - "#!lua name=" - + libName - + " \n redis.register_function('" - + funcName - + "', function(keys, args) return args[1] end)"; // function returns first argument + String code = generateLuaLibCode(libName, List.of(funcName)); assertEquals(libName, clusterClient.functionLoad(code, false).get()); // TODO test function with FCALL when fixed in redis-rs and implemented @@ -951,13 +943,15 @@ public void functionLoad_and_functionList_without_route() { // re-load library with overwriting assertEquals(libName, clusterClient.functionLoad(code, true).get()); String newFuncName = "myfunc2c"; - String newCode = - code - + "\n redis.register_function('" - + newFuncName - + "', function(keys, args) return #args end)"; // function returns argument count + String newCode = generateLuaLibCode(libName, List.of(funcName, newFuncName)); + assertEquals(libName, clusterClient.functionLoad(newCode, true).get()); + // load new lib and delete it - first lib remains loaded + String anotherLib = generateLuaLibCode("anotherLib", List.of("anotherFunc")); + assertEquals("anotherLib", clusterClient.functionLoad(anotherLib, true).get()); + assertEquals(OK, clusterClient.functionDelete("anotherLib").get()); + flist = clusterClient.functionList(libName, false).get(); expectedDescription.put(newFuncName, null); expectedFlags.put(newFuncName, Set.of()); diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index adafed5406..b71a1e7d01 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -4,6 +4,7 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestConfiguration.STANDALONE_PORTS; import static glide.TestUtilities.checkFunctionListResponse; +import static glide.TestUtilities.generateLuaLibCode; import static glide.TestUtilities.getValueFromInfo; import static glide.TestUtilities.parseInfoResponseToMap; import static glide.api.BaseClient.OK; @@ -32,6 +33,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -373,7 +375,7 @@ public void flushall() { @SneakyThrows @Test - public void functionLoad_and_functionList() { + public void function_commands() { assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7"); // TODO use FUNCTION FLUSH @@ -381,12 +383,7 @@ public void functionLoad_and_functionList() { String libName = "mylib1c"; String funcName = "myfunc1c"; - String code = - "#!lua name=" - + libName - + " \n redis.register_function('" - + funcName - + "', function(keys, args) return args[1] end)"; // function returns first argument + String code = generateLuaLibCode(libName, List.of(funcName)); assertEquals(libName, regularClient.functionLoad(code, false).get()); // TODO test function with FCALL when fixed in redis-rs and implemented @@ -419,13 +416,14 @@ public void functionLoad_and_functionList() { // re-load library with overwriting assertEquals(libName, regularClient.functionLoad(code, true).get()); String newFuncName = "myfunc2c"; - String newCode = - code - + "\n redis.register_function('" - + newFuncName - + "', function(keys, args) return #args end)"; // function returns argument count + String newCode = generateLuaLibCode(libName, List.of(funcName, newFuncName)); assertEquals(libName, regularClient.functionLoad(newCode, true).get()); + // load new lib and delete it - first lib remains loaded + String anotherLib = generateLuaLibCode("anotherLib", List.of("anotherFunc")); + assertEquals("anotherLib", regularClient.functionLoad(anotherLib, true).get()); + assertEquals(OK, regularClient.functionDelete("anotherLib").get()); + flist = regularClient.functionList(libName, false).get(); expectedDescription.put(newFuncName, null); expectedFlags.put(newFuncName, Set.of());