diff --git a/examples/java/JedisScriptingExample.java b/examples/java/JedisScriptingExample.java new file mode 100644 index 00000000000..c3652f05c69 --- /dev/null +++ b/examples/java/JedisScriptingExample.java @@ -0,0 +1,196 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.examples; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.args.FlushMode; + +/** + * Example demonstrating Jedis compatibility layer scripting features. Shows usage of EVAL, EVALSHA, + * SCRIPT management, FCALL, and FUNCTION commands. + */ +public class JedisScriptingExample { + + public static void main(String[] args) { + // Connect to Valkey server + try (Jedis jedis = new Jedis("localhost", 6379)) { + System.out.println("Connected to Valkey server"); + + // Lua scripting examples + demonstrateLuaScripting(jedis); + + // Function examples (requires Valkey 7.0+) + try { + demonstrateFunctions(jedis); + } catch (Exception e) { + System.out.println( + "Skipping function examples - requires Valkey 7.0+: " + e.getMessage()); + } + } + } + + /** Demonstrates Lua scripting with EVAL, EVALSHA, and SCRIPT commands. */ + private static void demonstrateLuaScripting(Jedis jedis) { + System.out.println("\n=== Lua Scripting Examples ==="); + + // 1. Simple EVAL + System.out.println("\n1. Simple EVAL:"); + Object result = jedis.eval("return 'Hello from Lua!'"); + System.out.println("Result: " + result); + + // 2. EVAL with keys and arguments + System.out.println("\n2. EVAL with keys and arguments:"); + jedis.set("mykey", "myvalue"); + Object value = jedis.eval("return redis.call('GET', KEYS[1])", 1, "mykey"); + System.out.println("Value from script: " + value); + + // 3. EVAL with multiple keys and arguments + System.out.println("\n3. EVAL with multiple keys and arguments:"); + String script = "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}"; + List keys = Arrays.asList("key1", "key2"); + List args = Arrays.asList("arg1", "arg2"); + Object multiResult = jedis.eval(script, keys, args); + if (multiResult instanceof Object[]) { + System.out.println("Results: " + Arrays.toString((Object[]) multiResult)); + } + + // 4. SCRIPT LOAD and EVALSHA + System.out.println("\n4. SCRIPT LOAD and EVALSHA:"); + String luaScript = "return ARGV[1] .. ' World!'"; + String sha1 = jedis.scriptLoad(luaScript); + System.out.println("Script SHA1: " + sha1); + + // Check if script exists + List exists = jedis.scriptExists(sha1); + System.out.println("Script exists: " + exists.get(0)); + + // Execute by SHA1 + Object shaResult = + jedis.evalsha(sha1, Collections.emptyList(), Collections.singletonList("Hello")); + System.out.println("EVALSHA result: " + shaResult); + + // 5. Complex script with Redis operations + System.out.println("\n5. Complex script with Redis operations:"); + jedis.set("counter", "0"); + String counterScript = + "redis.call('INCR', KEYS[1])\n" + + "redis.call('INCR', KEYS[1])\n" + + "return redis.call('GET', KEYS[1])"; + Object counterResult = jedis.eval(counterScript, 1, "counter"); + System.out.println("Counter after two increments: " + counterResult); + + // 6. SCRIPT management + System.out.println("\n6. SCRIPT management:"); + String anotherScript = "return 'test'"; + String sha2 = jedis.scriptLoad(anotherScript); + + // Check multiple scripts + List multiExists = jedis.scriptExists(sha1, sha2); + System.out.println("First script exists: " + multiExists.get(0)); + System.out.println("Second script exists: " + multiExists.get(1)); + + // Flush scripts + System.out.println("Flushing script cache..."); + String flushResult = jedis.scriptFlush(FlushMode.SYNC); + System.out.println("Flush result: " + flushResult); + + // Verify scripts are flushed + List afterFlush = jedis.scriptExists(sha1); + System.out.println("Script exists after flush: " + afterFlush.get(0)); + } + + /** + * Demonstrates Valkey Functions with FCALL, FUNCTION LOAD, and FUNCTION management commands. + * Requires Valkey 7.0 or higher. + */ + private static void demonstrateFunctions(Jedis jedis) { + System.out.println("\n=== Valkey Functions Examples ==="); + + // 1. Load a simple function + System.out.println("\n1. Loading a simple function:"); + String simpleLib = + "#!lua name=simplelib\n" + + "redis.register_function('greet', function(keys, args)\n" + + " return 'Hello, ' .. args[1]\n" + + "end)"; + String libName = jedis.functionLoad(simpleLib); + System.out.println("Loaded library: " + libName); + + // Call the function + Object greeting = + jedis.fcall("greet", Collections.emptyList(), Collections.singletonList("World")); + System.out.println("Function result: " + greeting); + + // 2. Load a function that uses keys and arguments + System.out.println("\n2. Function with keys and arguments:"); + String dataLib = + "#!lua name=datalib\n" + + "redis.register_function('setget', function(keys, args)\n" + + " redis.call('SET', keys[1], args[1])\n" + + " return redis.call('GET', keys[1])\n" + + "end)"; + jedis.functionLoad(dataLib); + + Object setGetResult = + jedis.fcall( + "setget", + Collections.singletonList("mykey"), + Collections.singletonList("myvalue")); + System.out.println("Set and get result: " + setGetResult); + + // 3. List functions + System.out.println("\n3. Listing functions:"); + List functions = jedis.functionList(); + System.out.println("Number of libraries loaded: " + functions.size()); + + // List functions with code + List functionsWithCode = jedis.functionListWithCode(); + System.out.println("Functions with code: " + functionsWithCode.size() + " libraries"); + + // 4. Function DUMP and RESTORE + System.out.println("\n4. Function DUMP and RESTORE:"); + byte[] dump = jedis.functionDump(); + System.out.println("Dumped " + dump.length + " bytes"); + + // Flush functions + jedis.functionFlush(); + System.out.println("Functions flushed"); + + // Restore from dump + String restoreResult = jedis.functionRestore(dump); + System.out.println("Restore result: " + restoreResult); + + // Verify restoration + Object verifyResult = + jedis.fcall("greet", Collections.emptyList(), Collections.singletonList("Again")); + System.out.println("Function after restore: " + verifyResult); + + // 5. Function stats + System.out.println("\n5. Function statistics:"); + Object stats = jedis.functionStats(); + System.out.println("Function stats retrieved: " + (stats != null)); + + // 6. Replace a function + System.out.println("\n6. Replacing a function:"); + String replacedLib = + "#!lua name=simplelib\n" + + "redis.register_function('greet', function(keys, args)\n" + + " return 'Hi, ' .. args[1] .. '!'\n" + + "end)"; + String replacedName = jedis.functionLoadReplace(replacedLib); + System.out.println("Replaced library: " + replacedName); + + Object newGreeting = + jedis.fcall( + "greet", Collections.emptyList(), Collections.singletonList("Replacement")); + System.out.println("New function result: " + newGreeting); + + // Clean up + System.out.println("\n7. Cleaning up:"); + jedis.functionDelete("simplelib"); + jedis.functionDelete("datalib"); + System.out.println("Libraries deleted"); + } +} diff --git a/java/integTest/src/test/java/compatibility/jedis/JedisScriptingIntegTest.java b/java/integTest/src/test/java/compatibility/jedis/JedisScriptingIntegTest.java new file mode 100644 index 00000000000..0d984f2d0df --- /dev/null +++ b/java/integTest/src/test/java/compatibility/jedis/JedisScriptingIntegTest.java @@ -0,0 +1,451 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package compatibility.jedis; + +import static glide.TestConfiguration.SERVER_VERSION; +import static glide.TestConfiguration.STANDALONE_HOSTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.*; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.args.FlushMode; +import redis.clients.jedis.args.FunctionRestorePolicy; +import redis.clients.jedis.resps.LibraryInfo; + +/** + * Integration tests for Jedis scripting and function commands. Tests EVAL, EVALSHA, SCRIPT + * management, FCALL, and FUNCTION management commands. + */ +public class JedisScriptingIntegTest { + + // Server configuration - dynamically resolved from CI environment + private static final String valkeyHost; + private static final int valkeyPort; + + private Jedis jedis; + + static { + String[] standaloneHosts = STANDALONE_HOSTS; + + if (standaloneHosts.length == 0 || standaloneHosts[0].trim().isEmpty()) { + throw new IllegalStateException( + "Standalone server configuration not found in system properties. " + + "Please set 'test.server.standalone' system property with server address " + + "(e.g., -Dtest.server.standalone=localhost:6379)"); + } + + String firstHost = standaloneHosts[0].trim(); + String[] hostPort = firstHost.split(":"); + + if (hostPort.length == 2) { + try { + valkeyHost = hostPort[0]; + valkeyPort = Integer.parseInt(hostPort[1]); + } catch (NumberFormatException e) { + throw new IllegalStateException( + "Invalid port number in standalone server configuration: " + + firstHost + + ". " + + "Expected format: host:port (e.g., localhost:6379)", + e); + } + } else { + throw new IllegalStateException( + "Invalid standalone server format: " + + firstHost + + ". " + + "Expected format: host:port (e.g., localhost:6379)"); + } + } + + @BeforeEach + void setup() { + jedis = new Jedis(valkeyHost, valkeyPort); + jedis.connect(); + assertNotNull(jedis, "Jedis instance should be created successfully"); + } + + @AfterEach + void teardown() { + if (jedis != null) { + jedis.close(); + } + } + + @Test + void testEvalBasic() { + Object result = jedis.eval("return 'hello'"); + assertEquals("hello", result); + } + + @Test + void testEvalWithScript() { + Object result = jedis.eval("return 42"); + assertEquals(42L, result); + } + + @Test + void testEvalWithKeys() { + jedis.set("key1", "value1"); + Object result = jedis.eval("return redis.call('GET', KEYS[1])", 1, "key1"); + assertEquals("value1", result); + } + + @Test + void testEvalWithKeysAndArgs() { + Object result = + jedis.eval( + "return {KEYS[1], ARGV[1]}", + Collections.singletonList("mykey"), + Collections.singletonList("myarg")); + assertTrue(result instanceof Object[]); + Object[] arr = (Object[]) result; + assertEquals(2, arr.length); + assertEquals("mykey", arr[0]); + assertEquals("myarg", arr[1]); + } + + @Test + void testEvalWithMultipleKeysAndArgs() { + List keys = Arrays.asList("key1", "key2"); + List args = Arrays.asList("arg1", "arg2", "arg3"); + Object result = jedis.eval("return {KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3]}", keys, args); + assertTrue(result instanceof Object[]); + Object[] arr = (Object[]) result; + assertEquals(5, arr.length); + } + + @Test + void testScriptLoadAndEvalsha() { + String script = "return ARGV[1]"; + String sha1 = jedis.scriptLoad(script); + assertNotNull(sha1); + assertEquals(40, sha1.length()); // SHA1 is 40 characters + + List exists = jedis.scriptExists(sha1); + assertNotNull(exists); + assertEquals(1, exists.size()); + assertTrue(exists.get(0)); + + Object result = jedis.evalsha(sha1, Collections.emptyList(), Collections.singletonList("test")); + assertEquals("test", result); + } + + @Test + void testScriptExists() { + String script1 = "return 1"; + String script2 = "return 2"; + + String sha1 = jedis.scriptLoad(script1); + String sha2 = jedis.scriptLoad(script2); + + List exists = jedis.scriptExists(sha1, sha2); + assertEquals(2, exists.size()); + assertTrue(exists.get(0)); + assertTrue(exists.get(1)); + + // Check non-existent script + String fakeSha = "0000000000000000000000000000000000000000"; + List exists2 = jedis.scriptExists(fakeSha); + assertEquals(1, exists2.size()); + assertFalse(exists2.get(0)); + } + + @Test + void testScriptFlush() { + String script = "return 'test'"; + String sha1 = jedis.scriptLoad(script); + + List existsBefore = jedis.scriptExists(sha1); + assertTrue(existsBefore.get(0)); + + String result = jedis.scriptFlush(); + assertEquals("OK", result); + + List existsAfter = jedis.scriptExists(sha1); + assertFalse(existsAfter.get(0)); + } + + @Test + void testScriptFlushWithMode() { + String script = "return 'test'"; + jedis.scriptLoad(script); + + String result = jedis.scriptFlush(FlushMode.SYNC); + assertEquals("OK", result); + } + + @Test + void testEvalReadonly() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for EVAL_RO"); + + jedis.set("key1", "value1"); + Object result = + jedis.evalReadonly( + "return redis.call('GET', KEYS[1])", + Collections.singletonList("key1"), + Collections.emptyList()); + assertEquals("value1", result); + } + + @Test + void testEvalshaReadonly() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for EVALSHA_RO"); + + jedis.set("key1", "value1"); + String script = "return redis.call('GET', KEYS[1])"; + String sha1 = jedis.scriptLoad(script); + + Object result = + jedis.evalshaReadonly(sha1, Collections.singletonList("key1"), Collections.emptyList()); + assertEquals("value1", result); + } + + @Test + void testFunctionLoadAndCall() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib = + "#!lua name=mylib\n" + + "redis.register_function('myfunc', function(keys, args) return args[1] end)"; + String libName = jedis.functionLoad(lib); + assertEquals("mylib", libName); + + Object result = jedis.fcall("myfunc", Collections.emptyList(), Collections.singletonList("42")); + assertEquals("42", result); + + // Clean up + jedis.functionDelete("mylib"); + } + + @Test + void testFunctionLoadReplace() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib1 = + "#!lua name=replacelib\n" + + "redis.register_function('func1', function(keys, args) return 1 end)"; + jedis.functionLoad(lib1); + + String lib2 = + "#!lua name=replacelib\n" + + "redis.register_function('func2', function(keys, args) return 2 end)"; + String libName = jedis.functionLoadReplace(lib2); + assertEquals("replacelib", libName); + + Object result = jedis.fcall("func2", Collections.emptyList(), Collections.emptyList()); + assertEquals(2L, result); + + // Clean up + jedis.functionDelete("replacelib"); + } + + @Test + void testFunctionList() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib = + "#!lua name=listlib\n" + + "redis.register_function('listfunc', function(keys, args) return 1 end)"; + jedis.functionLoad(lib); + + List functions = jedis.functionList(); + assertNotNull(functions); + assertTrue(functions.size() > 0); + + // Clean up + jedis.functionDelete("listlib"); + } + + @Test + void testFunctionListWithPattern() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib = + "#!lua name=patternlib\n" + + "redis.register_function('patternfunc', function(keys, args) return 1 end)"; + jedis.functionLoad(lib); + + List functions = jedis.functionList("pattern*"); + assertNotNull(functions); + + // Clean up + jedis.functionDelete("patternlib"); + } + + @Test + void testFunctionListWithCode() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib = + "#!lua name=codelib\n" + + "redis.register_function('codefunc', function(keys, args) return 1 end)"; + jedis.functionLoad(lib); + + List functions = jedis.functionListWithCode(); + assertNotNull(functions); + assertTrue(functions.size() > 0); + + // Clean up + jedis.functionDelete("codelib"); + } + + @Test + void testFunctionDumpAndRestore() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib = + "#!lua name=dumplib\n" + + "redis.register_function('dumpfunc', function(keys, args) return 1 end)"; + jedis.functionLoad(lib); + + byte[] dump = jedis.functionDump(); + assertNotNull(dump); + assertTrue(dump.length > 0); + + // Flush and restore + jedis.functionFlush(); + String result = jedis.functionRestore(dump); + assertEquals("OK", result); + + // Verify function is restored + Object callResult = jedis.fcall("dumpfunc", Collections.emptyList(), Collections.emptyList()); + assertEquals(1L, callResult); + + // Clean up + jedis.functionDelete("dumplib"); + } + + @Test + void testFunctionRestoreWithPolicy() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib = + "#!lua name=policylib\n" + + "redis.register_function('policyfunc', function(keys, args) return 1 end)"; + jedis.functionLoad(lib); + + byte[] dump = jedis.functionDump(); + + // Restore with FLUSH policy + String result = jedis.functionRestore(dump, FunctionRestorePolicy.FLUSH); + assertEquals("OK", result); + + // Clean up + jedis.functionDelete("policylib"); + } + + @Test + void testFunctionFlush() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib = + "#!lua name=flushlib\n" + + "redis.register_function('flushfunc', function(keys, args) return 1 end)"; + jedis.functionLoad(lib); + + String result = jedis.functionFlush(); + assertEquals("OK", result); + + List functions = jedis.functionList(); + assertEquals(0, functions.size()); + } + + @Test + void testFunctionFlushWithMode() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib = + "#!lua name=flushmodelib\n" + + "redis.register_function('flushmodefunc', function(keys, args) return 1 end)"; + jedis.functionLoad(lib); + + String result = jedis.functionFlush(FlushMode.SYNC); + assertEquals("OK", result); + } + + @Test + void testFunctionDelete() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib = + "#!lua name=deletelib\n" + + "redis.register_function('deletefunc', function(keys, args) return 1 end)"; + jedis.functionLoad(lib); + + String result = jedis.functionDelete("deletelib"); + assertEquals("OK", result); + } + + @Test + void testFunctionStats() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + Object stats = jedis.functionStats(); + assertNotNull(stats); + } + + @Test + void testFcallReadonly() { + // Requires Valkey 7.0+ + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo("7.0.0"), + "Valkey version 7.0 or higher is required for functions"); + + String lib = + "#!lua name=rolib\n" + + "redis.register_function{function_name='rofunc', callback=function(keys, args) return" + + " args[1] end, flags={'no-writes'}}"; + jedis.functionLoad(lib); + + Object result = + jedis.fcallReadonly( + "rofunc", Collections.emptyList(), Collections.singletonList("readonly")); + assertEquals("readonly", result); + + // Clean up + jedis.functionDelete("rolib"); + } +} diff --git a/java/jedis-compatibility/README.md b/java/jedis-compatibility/README.md index f27ba7f7b78..e9a53391448 100644 --- a/java/jedis-compatibility/README.md +++ b/java/jedis-compatibility/README.md @@ -63,6 +63,79 @@ try (Jedis jedis = new Jedis("localhost", 6379)) { } ``` +### Scripting Commands + +The compatibility layer supports Lua scripting and Valkey Functions: + +#### Lua Scripts + +```java +import redis.clients.jedis.Jedis; +import java.util.Collections; +import java.util.List; + +try (Jedis jedis = new Jedis("localhost", 6379)) { + // Execute Lua script directly + Object result = jedis.eval("return 'Hello'"); + + // Execute with keys and arguments + jedis.set("mykey", "myvalue"); + Object value = jedis.eval( + "return redis.call('GET', KEYS[1])", + 1, + "mykey" + ); + + // Load script and execute by SHA + String sha1 = jedis.scriptLoad("return ARGV[1]"); + Object result2 = jedis.evalsha(sha1, 0, "test"); + + // Check if scripts exist + List exists = jedis.scriptExists(sha1); + System.out.println("Script exists: " + exists.get(0)); +} +``` + +#### Valkey Functions (7.0+) + +```java +import redis.clients.jedis.Jedis; +import java.util.Collections; +import java.util.List; + +try (Jedis jedis = new Jedis("localhost", 6379)) { + // Load a function library + String lib = "#!lua name=mylib\n" + + "redis.register_function('myfunc', " + + "function(keys, args) return args[1] end)"; + String libName = jedis.functionLoad(lib); + + // Call the function + Object result = jedis.fcall( + "myfunc", + Collections.emptyList(), + Collections.singletonList("42") + ); + System.out.println("Result: " + result); // prints: 42 + + // List loaded functions + List functions = jedis.functionList(); + + // Clean up + jedis.functionDelete("mylib"); +} +``` + +#### Implementation Notes + +The scripting commands are implemented using a combination of: +- **Type-safe GLIDE APIs** for most operations (e.g., `scriptExists`, `scriptFlush`, `evalReadOnly`, `fcall`, `functionLoad`) +- **`customCommand`** for operations not yet exposed in GLIDE's type-safe API: + - `SCRIPT LOAD` - Required to explicitly load scripts to the server cache + - `EVALSHA` (non-readonly) - Standard EVALSHA that allows write operations + +This hybrid approach ensures full Jedis API compatibility while leveraging GLIDE's type-safe APIs wherever available. + ## Migration Guide See the [compatibility layer migration guide](./compatibility-layer-migration-guide.md) for detailed migration instructions. diff --git a/java/jedis-compatibility/compatibility-layer-migration-guide.md b/java/jedis-compatibility/compatibility-layer-migration-guide.md index 0b800bb23b4..215731ed852 100644 --- a/java/jedis-compatibility/compatibility-layer-migration-guide.md +++ b/java/jedis-compatibility/compatibility-layer-migration-guide.md @@ -98,7 +98,7 @@ blockingSocketTimeoutMillis - **Transactions**: MULTI/EXEC transaction blocks not supported - **Pipelining**: Jedis pipelining functionality unavailable - **Pub/Sub**: Redis publish/subscribe not implemented -- **Lua scripting**: EVAL/EVALSHA commands not supported +- ✅ **Lua scripting**: Full support for EVAL/EVALSHA, SCRIPT management, and Valkey Functions (FCALL/FUNCTION *) - **Modules**: Redis module commands not available - **Typed sorted set methods**: No dedicated methods like `zadd()`, `zrem()` - use `sendCommand()` instead diff --git a/java/jedis-compatibility/src/main/java/redis/clients/jedis/Jedis.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/Jedis.java index 21c0e43b0f1..99f234026da 100644 --- a/java/jedis-compatibility/src/main/java/redis/clients/jedis/Jedis.java +++ b/java/jedis-compatibility/src/main/java/redis/clients/jedis/Jedis.java @@ -3,10 +3,12 @@ import glide.api.GlideClient; import glide.api.models.GlideString; +import glide.api.models.Script; import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.GetExOptions; import glide.api.models.commands.LInsertOptions.InsertPosition; import glide.api.models.commands.LPosOptions; +import glide.api.models.commands.ScriptOptions; import glide.api.models.commands.SetOptions; import glide.api.models.commands.SortBaseOptions; import glide.api.models.commands.SortOptions; @@ -49,6 +51,8 @@ import redis.clients.jedis.args.BitCountOption; import redis.clients.jedis.args.BitOP; import redis.clients.jedis.args.ExpiryOption; +import redis.clients.jedis.args.FlushMode; +import redis.clients.jedis.args.FunctionRestorePolicy; import redis.clients.jedis.args.ListDirection; import redis.clients.jedis.args.ListPosition; import redis.clients.jedis.commands.ProtocolCommand; @@ -63,6 +67,8 @@ import redis.clients.jedis.params.SetParams; import redis.clients.jedis.resps.AccessControlLogEntry; import redis.clients.jedis.resps.AccessControlUser; +import redis.clients.jedis.resps.FunctionStats; +import redis.clients.jedis.resps.LibraryInfo; import redis.clients.jedis.resps.ScanResult; import redis.clients.jedis.util.KeyValue; import redis.clients.jedis.util.Pool; @@ -7299,6 +7305,528 @@ public byte[] brpoplpush(final byte[] source, final byte[] destination, int time return blmove(source, destination, ListDirection.RIGHT, ListDirection.LEFT, timeout); } + // ==================== Scripting and Functions Commands ==================== + + /** + * Executes a Lua script on the server. + * + * @param script the Lua 5.1 script to execute + * @return the result of the script execution + * @see EVAL + */ + public Object eval(String script) { + return eval(script, Collections.emptyList(), Collections.emptyList()); + } + + /** + * Executes a Lua script on the server with keys and arguments. + * + * @param script the Lua 5.1 script to execute + * @param keyCount the number of keys (first keyCount params are keys, rest are arguments) + * @param params the keys and arguments for the script + * @return the result of the script execution + * @see EVAL + */ + public Object eval(String script, int keyCount, String... params) { + List keys = new ArrayList<>(); + List args = new ArrayList<>(); + for (int i = 0; i < params.length; i++) { + if (i < keyCount) { + keys.add(params[i]); + } else { + args.add(params[i]); + } + } + return eval(script, keys, args); + } + + /** + * Executes a Lua script on the server with keys and arguments. + * + * @param script the Lua 5.1 script to execute + * @param keys the keys accessed by the script + * @param args the arguments for the script + * @return the result of the script execution + * @see EVAL + */ + public Object eval(String script, List keys, List args) { + return executeCommandWithGlide( + "EVAL", + () -> { + try (Script luaScript = new Script(script, false)) { + ScriptOptions.ScriptOptionsBuilder builder = ScriptOptions.builder(); + if (keys != null && !keys.isEmpty()) { + for (String key : keys) { + builder.key(key); + } + } + if (args != null && !args.isEmpty()) { + for (String arg : args) { + builder.arg(arg); + } + } + ScriptOptions options = builder.build(); + return glideClient.invokeScript(luaScript, options).get(); + } catch (Exception e) { + throw new RuntimeException("Failed to execute script", e); + } + }); + } + + /** + * Executes a Lua script by its SHA1 digest. + * + * @param sha1 the SHA1 digest of the script + * @return the result of the script execution + * @see EVALSHA + */ + public Object evalsha(String sha1) { + return evalsha(sha1, Collections.emptyList(), Collections.emptyList()); + } + + /** + * Executes a Lua script by its SHA1 digest with keys and arguments. + * + * @param sha1 the SHA1 digest of the script + * @param keyCount the number of keys (first keyCount params are keys, rest are arguments) + * @param params the keys and arguments for the script + * @return the result of the script execution + * @see EVALSHA + */ + public Object evalsha(String sha1, int keyCount, String... params) { + List keys = new ArrayList<>(); + List args = new ArrayList<>(); + for (int i = 0; i < params.length; i++) { + if (i < keyCount) { + keys.add(params[i]); + } else { + args.add(params[i]); + } + } + return evalsha(sha1, keys, args); + } + + /** + * Executes a Lua script by its SHA1 digest with keys and arguments. + * + *

Implementation Note: This method uses {@code customCommand} because GLIDE Java does + * not currently expose a type-safe {@code evalsha} API for non-read-only operations. While GLIDE + * provides {@link #evalshaReadonly(String, List, List)} for read-only scripts, the standard + * {@code EVALSHA} command (which allows writes) must be sent using {@code customCommand}. + * + * @param sha1 the SHA1 digest of the script + * @param keys the keys accessed by the script + * @param args the arguments for the script + * @return the result of the script execution + * @see EVALSHA + */ + public Object evalsha(String sha1, List keys, List args) { + return executeCommandWithGlide( + "EVALSHA", + () -> { + // Use customCommand since GLIDE Java only exposes evalshaReadOnly, not evalsha + // Build the command: EVALSHA sha1 numkeys key [key ...] arg [arg ...] + List cmdArgs = new ArrayList<>(); + cmdArgs.add("EVALSHA"); + cmdArgs.add(sha1); + cmdArgs.add(String.valueOf(keys != null ? keys.size() : 0)); + if (keys != null) { + cmdArgs.addAll(keys); + } + if (args != null) { + cmdArgs.addAll(args); + } + return glideClient.customCommand(cmdArgs.toArray(new String[0])).get(); + }); + } + + /** + * Executes a read-only Lua script with keys and arguments. + * + * @param script the Lua 5.1 script to execute + * @param keys the keys accessed by the script + * @param args the arguments for the script + * @return the result of the script execution + * @see EVAL_RO + * @since Valkey 7.0 and above + */ + public Object evalReadonly(String script, List keys, List args) { + return executeCommandWithGlide( + "EVAL_RO", + () -> { + String[] keyArray = keys != null ? keys.toArray(new String[0]) : new String[0]; + String[] argArray = args != null ? args.toArray(new String[0]) : new String[0]; + return glideClient.evalReadOnly(script, keyArray, argArray).get(); + }); + } + + /** + * Executes a read-only Lua script by its SHA1 digest with keys and arguments. + * + * @param sha1 the SHA1 digest of the script + * @param keys the keys accessed by the script + * @param args the arguments for the script + * @return the result of the script execution + * @see EVALSHA_RO + * @since Valkey 7.0 and above + */ + public Object evalshaReadonly(String sha1, List keys, List args) { + return executeCommandWithGlide( + "EVALSHA_RO", + () -> { + String[] keyArray = keys != null ? keys.toArray(new String[0]) : new String[0]; + String[] argArray = args != null ? args.toArray(new String[0]) : new String[0]; + return glideClient.evalshaReadOnly(sha1, keyArray, argArray).get(); + }); + } + + /** + * Loads a Lua script into the server's script cache and returns its SHA1 digest. + * + *

Implementation Note: This method uses {@code customCommand} because GLIDE Java does + * not currently expose a type-safe {@code scriptLoad} API. While GLIDE's {@link Script} object + * can store scripts in the Rust FFI layer and compute SHA1 hashes client-side, it does not + * explicitly send {@code SCRIPT LOAD} to the Valkey server, which is required for Jedis API + * compatibility where scripts must be loaded before {@code EVALSHA} can be used. + * + * @param script the Lua script to load + * @return the SHA1 digest of the script + * @see SCRIPT LOAD + */ + public String scriptLoad(String script) { + return executeCommandWithGlide( + "SCRIPT LOAD", + () -> { + // Use customCommand since GLIDE Java doesn't expose a type-safe scriptLoad API + return (String) glideClient.customCommand(new String[] {"SCRIPT", "LOAD", script}).get(); + }); + } + + /** + * Checks if scripts exist in the script cache by their SHA1 digests. + * + * @param sha1 the SHA1 digests to check + * @return a list of booleans indicating the existence of each script + * @see SCRIPT EXISTS + */ + public List scriptExists(String... sha1) { + return executeCommandWithGlide( + "SCRIPT EXISTS", + () -> { + Boolean[] result = glideClient.scriptExists(sha1).get(); + return Arrays.asList(result); + }); + } + + /** + * Flushes the Lua scripts cache. + * + * @return "OK" + * @see SCRIPT FLUSH + */ + public String scriptFlush() { + return executeCommandWithGlide("SCRIPT FLUSH", () -> glideClient.scriptFlush().get()); + } + + /** + * Flushes the Lua scripts cache with the specified flush mode. + * + * @param flushMode the flush mode (SYNC or ASYNC) + * @return "OK" + * @see SCRIPT FLUSH + */ + public String scriptFlush(FlushMode flushMode) { + return executeCommandWithGlide( + "SCRIPT FLUSH", () -> glideClient.scriptFlush(flushMode.toGlideFlushMode()).get()); + } + + /** + * Kills the currently executing Lua script, assuming no write operation was yet performed by the + * script. + * + * @return "OK" + * @see SCRIPT KILL + */ + public String scriptKill() { + return executeCommandWithGlide("SCRIPT KILL", () -> glideClient.scriptKill().get()); + } + + /** + * Loads a library to Valkey. + * + * @param functionCode the source code that implements the library + * @return the library name that was loaded + * @see FUNCTION LOAD + * @since Valkey 7.0 and above + */ + public String functionLoad(String functionCode) { + return executeCommandWithGlide( + "FUNCTION LOAD", () -> glideClient.functionLoad(functionCode, false).get()); + } + + /** + * Loads a library to Valkey, replacing any existing library with the same name. + * + * @param functionCode the source code that implements the library + * @return the library name that was loaded + * @see FUNCTION LOAD + * @since Valkey 7.0 and above + */ + public String functionLoadReplace(String functionCode) { + return executeCommandWithGlide( + "FUNCTION LOAD", () -> glideClient.functionLoad(functionCode, true).get()); + } + + /** + * Deletes a library and all its functions. + * + * @param libraryName the library name to delete + * @return "OK" + * @see FUNCTION DELETE + * @since Valkey 7.0 and above + */ + public String functionDelete(String libraryName) { + return executeCommandWithGlide( + "FUNCTION DELETE", () -> glideClient.functionDelete(libraryName).get()); + } + + /** + * Returns the serialized payload of all loaded libraries. + * + * @return the serialized payload of all loaded libraries + * @see FUNCTION DUMP + * @since Valkey 7.0 and above + */ + public byte[] functionDump() { + return executeCommandWithGlide("FUNCTION DUMP", () -> glideClient.functionDump().get()); + } + + /** + * Restores libraries from the serialized payload. + * + * @param serializedValue the serialized data from functionDump + * @return "OK" + * @see FUNCTION RESTORE + * @since Valkey 7.0 and above + */ + public String functionRestore(byte[] serializedValue) { + return executeCommandWithGlide( + "FUNCTION RESTORE", () -> glideClient.functionRestore(serializedValue).get()); + } + + /** + * Restores libraries from the serialized payload with a policy for handling existing libraries. + * + * @param serializedValue the serialized data from functionDump + * @param policy the policy for handling existing libraries + * @return "OK" + * @see FUNCTION RESTORE + * @since Valkey 7.0 and above + */ + public String functionRestore(byte[] serializedValue, FunctionRestorePolicy policy) { + return executeCommandWithGlide( + "FUNCTION RESTORE", + () -> + glideClient + .functionRestore(serializedValue, policy.toGlideFunctionRestorePolicy()) + .get()); + } + + /** + * Deletes all function libraries. + * + * @return "OK" + * @see FUNCTION FLUSH + * @since Valkey 7.0 and above + */ + public String functionFlush() { + return executeCommandWithGlide("FUNCTION FLUSH", () -> glideClient.functionFlush().get()); + } + + /** + * Deletes all function libraries with the specified flush mode. + * + * @param mode the flushing mode (SYNC or ASYNC) + * @return "OK" + * @see FUNCTION FLUSH + * @since Valkey 7.0 and above + */ + public String functionFlush(FlushMode mode) { + return executeCommandWithGlide( + "FUNCTION FLUSH", () -> glideClient.functionFlush(mode.toGlideFlushMode()).get()); + } + + /** + * Kills a function that is currently executing. + * + * @return "OK" if function is terminated + * @see FUNCTION KILL + * @since Valkey 7.0 and above + */ + public String functionKill() { + return executeCommandWithGlide("FUNCTION KILL", () -> glideClient.functionKill().get()); + } + + /** + * Invokes a previously loaded function. + * + * @param name the function name + * @param keys the keys accessed by the function + * @param args the function arguments + * @return the invoked function's return value + * @see FCALL + * @since Valkey 7.0 and above + */ + public Object fcall(String name, List keys, List args) { + return executeCommandWithGlide( + "FCALL", + () -> { + String[] keyArray = keys != null ? keys.toArray(new String[0]) : new String[0]; + String[] argArray = args != null ? args.toArray(new String[0]) : new String[0]; + return glideClient.fcall(name, keyArray, argArray).get(); + }); + } + + /** + * Invokes a previously loaded read-only function. + * + * @param name the function name + * @param keys the keys accessed by the function + * @param args the function arguments + * @return the invoked function's return value + * @see FCALL_RO + * @since Valkey 7.0 and above + */ + public Object fcallReadonly(String name, List keys, List args) { + return executeCommandWithGlide( + "FCALL_RO", + () -> { + String[] keyArray = keys != null ? keys.toArray(new String[0]) : new String[0]; + String[] argArray = args != null ? args.toArray(new String[0]) : new String[0]; + return glideClient.fcallReadOnly(name, keyArray, argArray).get(); + }); + } + + /** + * Returns information about all loaded libraries. + * + * @return info about all libraries and their functions + * @see FUNCTION LIST + * @since Valkey 7.0 and above + */ + public List functionList() { + return executeCommandWithGlide( + "FUNCTION LIST", + () -> { + Map[] result = glideClient.functionList(false).get(); + List libraries = new ArrayList<>(result.length); + for (Map lib : result) { + libraries.add(new LibraryInfo(lib)); + } + return libraries; + }); + } + + /** + * Returns information about loaded libraries matching a pattern. + * + * @param libraryNamePattern a wildcard pattern for matching library names + * @return info about queried libraries and their functions + * @see FUNCTION LIST + * @since Valkey 7.0 and above + */ + public List functionList(String libraryNamePattern) { + return executeCommandWithGlide( + "FUNCTION LIST", + () -> { + Map[] result = glideClient.functionList(libraryNamePattern, false).get(); + List libraries = new ArrayList<>(result.length); + for (Map lib : result) { + libraries.add(new LibraryInfo(lib)); + } + return libraries; + }); + } + + /** + * Returns information about all loaded libraries with their code. + * + * @return info about all libraries and their functions including code + * @see FUNCTION LIST + * @since Valkey 7.0 and above + */ + public List functionListWithCode() { + return executeCommandWithGlide( + "FUNCTION LIST", + () -> { + Map[] result = glideClient.functionList(true).get(); + List libraries = new ArrayList<>(result.length); + for (Map lib : result) { + libraries.add(new LibraryInfo(lib)); + } + return libraries; + }); + } + + /** + * Returns information about loaded libraries matching a pattern with their code. + * + * @param libraryNamePattern a wildcard pattern for matching library names + * @return info about queried libraries and their functions including code + * @see FUNCTION LIST + * @since Valkey 7.0 and above + */ + public List functionListWithCode(String libraryNamePattern) { + return executeCommandWithGlide( + "FUNCTION LIST", + () -> { + Map[] result = glideClient.functionList(libraryNamePattern, true).get(); + List libraries = new ArrayList<>(result.length); + for (Map lib : result) { + libraries.add(new LibraryInfo(lib)); + } + return libraries; + }); + } + + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * @return a map with information about running scripts and available engines + * @see FUNCTION STATS + * @since Valkey 7.0 and above + */ + @SuppressWarnings("unchecked") + public FunctionStats functionStats() { + return executeCommandWithGlide( + "FUNCTION STATS", + () -> { + Map>> result = glideClient.functionStats().get(); + // The result structure is: { "running_script": {...}, "engines": {...} } + // But GLIDE returns it as Map>> + // We need to extract and flatten appropriately + Map runningScript = null; + Map> engines = null; + + if (result != null) { + // Get running_script - it's actually a Map> + Object runningScriptObj = result.get("running_script"); + if (runningScriptObj instanceof Map) { + runningScript = (Map) runningScriptObj; + } + + // Get engines - it's a Map> + Object enginesObj = result.get("engines"); + if (enginesObj instanceof Map) { + engines = (Map>) enginesObj; + } + } + + return new FunctionStats(runningScript, engines); + }); + } + /** * Adds the specified members to the set stored at key. * diff --git a/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/FlushMode.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/FlushMode.java new file mode 100644 index 00000000000..aa7a7b2c367 --- /dev/null +++ b/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/FlushMode.java @@ -0,0 +1,33 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package redis.clients.jedis.args; + +/** + * Flush mode for FLUSHDB, FLUSHALL, FUNCTION FLUSH, and SCRIPT FLUSH commands. + * + * @see FLUSHALL + * @see FLUSHDB + * @see FUNCTION FLUSH + * @see SCRIPT FLUSH + */ +public enum FlushMode implements Rawable { + /** Flushes synchronously */ + SYNC, + /** Flushes asynchronously */ + ASYNC; + + @Override + public byte[] getRaw() { + return name().getBytes(); + } + + /** + * Convert to GLIDE FlushMode. + * + * @return The equivalent GLIDE FlushMode + */ + public glide.api.models.commands.FlushMode toGlideFlushMode() { + return this == SYNC + ? glide.api.models.commands.FlushMode.SYNC + : glide.api.models.commands.FlushMode.ASYNC; + } +} diff --git a/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/FunctionRestorePolicy.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/FunctionRestorePolicy.java new file mode 100644 index 00000000000..154a5222f23 --- /dev/null +++ b/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/FunctionRestorePolicy.java @@ -0,0 +1,39 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package redis.clients.jedis.args; + +/** + * Policy options for FUNCTION RESTORE command. + * + * @see FUNCTION RESTORE + */ +public enum FunctionRestorePolicy implements Rawable { + /** Appends the restored libraries to the existing libraries and aborts on collision (default) */ + APPEND, + /** Deletes all existing libraries before restoring the payload */ + FLUSH, + /** Appends the restored libraries, replacing any existing ones in case of name collisions */ + REPLACE; + + @Override + public byte[] getRaw() { + return name().getBytes(); + } + + /** + * Convert to GLIDE FunctionRestorePolicy. + * + * @return The equivalent GLIDE FunctionRestorePolicy + */ + public glide.api.models.commands.function.FunctionRestorePolicy toGlideFunctionRestorePolicy() { + switch (this) { + case APPEND: + return glide.api.models.commands.function.FunctionRestorePolicy.APPEND; + case FLUSH: + return glide.api.models.commands.function.FunctionRestorePolicy.FLUSH; + case REPLACE: + return glide.api.models.commands.function.FunctionRestorePolicy.REPLACE; + default: + throw new IllegalStateException("Unknown FunctionRestorePolicy: " + this); + } + } +} diff --git a/java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisMethodsTest.java b/java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisMethodsTest.java index 07e6fda5dd7..dcd01ecf7aa 100644 --- a/java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisMethodsTest.java +++ b/java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisMethodsTest.java @@ -8,6 +8,7 @@ import java.util.Set; import org.junit.jupiter.api.Test; import redis.clients.jedis.resps.AccessControlUser; +import redis.clients.jedis.resps.FunctionStats; /** * Unit tests for Jedis method signatures and API contracts. Tests that required methods exist with @@ -128,6 +129,166 @@ public void testJedisStateManagementMethods() throws NoSuchMethodException { assertEquals(void.class, closeMethod.getReturnType()); } + @Test + public void testEvalMethodSignatures() throws NoSuchMethodException { + Class jedisClass = Jedis.class; + + // Test eval(String) exists + Method evalSimple = jedisClass.getMethod("eval", String.class); + assertEquals(Object.class, evalSimple.getReturnType()); + + // Test eval(String, int, String...) exists + Method evalWithKeys = jedisClass.getMethod("eval", String.class, int.class, String[].class); + assertEquals(Object.class, evalWithKeys.getReturnType()); + + // Test eval(String, List, List) exists + Method evalWithLists = jedisClass.getMethod("eval", String.class, List.class, List.class); + assertEquals(Object.class, evalWithLists.getReturnType()); + } + + @Test + public void testEvalshaMethodSignatures() throws NoSuchMethodException { + Class jedisClass = Jedis.class; + + // Test evalsha(String) exists + Method evalshaSimple = jedisClass.getMethod("evalsha", String.class); + assertEquals(Object.class, evalshaSimple.getReturnType()); + + // Test evalsha(String, int, String...) exists + Method evalshaWithKeys = + jedisClass.getMethod("evalsha", String.class, int.class, String[].class); + assertEquals(Object.class, evalshaWithKeys.getReturnType()); + + // Test evalsha(String, List, List) exists + Method evalshaWithLists = jedisClass.getMethod("evalsha", String.class, List.class, List.class); + assertEquals(Object.class, evalshaWithLists.getReturnType()); + } + + @Test + public void testEvalReadonlyMethodSignatures() throws NoSuchMethodException { + Class jedisClass = Jedis.class; + + // Test evalReadonly(String, List, List) exists + Method evalReadonly = + jedisClass.getMethod("evalReadonly", String.class, List.class, List.class); + assertEquals(Object.class, evalReadonly.getReturnType()); + + // Test evalshaReadonly(String, List, List) exists + Method evalshaReadonly = + jedisClass.getMethod("evalshaReadonly", String.class, List.class, List.class); + assertEquals(Object.class, evalshaReadonly.getReturnType()); + } + + @Test + public void testScriptManagementMethodSignatures() throws NoSuchMethodException { + Class jedisClass = Jedis.class; + + // Test scriptLoad(String) exists + Method scriptLoad = jedisClass.getMethod("scriptLoad", String.class); + assertEquals(String.class, scriptLoad.getReturnType()); + + // Test scriptExists(String...) exists + Method scriptExists = jedisClass.getMethod("scriptExists", String[].class); + assertEquals(List.class, scriptExists.getReturnType()); + + // Test scriptFlush() exists + Method scriptFlush = jedisClass.getMethod("scriptFlush"); + assertEquals(String.class, scriptFlush.getReturnType()); + + // Test scriptFlush(FlushMode) exists + Method scriptFlushWithMode = + jedisClass.getMethod("scriptFlush", redis.clients.jedis.args.FlushMode.class); + assertEquals(String.class, scriptFlushWithMode.getReturnType()); + + // Test scriptKill() exists + Method scriptKill = jedisClass.getMethod("scriptKill"); + assertEquals(String.class, scriptKill.getReturnType()); + } + + @Test + public void testFcallMethodSignatures() throws NoSuchMethodException { + Class jedisClass = Jedis.class; + + // Test fcall(String, List, List) exists + Method fcall = jedisClass.getMethod("fcall", String.class, List.class, List.class); + assertEquals(Object.class, fcall.getReturnType()); + + // Test fcallReadonly(String, List, List) exists + Method fcallReadonly = + jedisClass.getMethod("fcallReadonly", String.class, List.class, List.class); + assertEquals(Object.class, fcallReadonly.getReturnType()); + } + + @Test + public void testFunctionManagementMethodSignatures() throws NoSuchMethodException { + Class jedisClass = Jedis.class; + + // Test functionLoad(String) exists + Method functionLoad = jedisClass.getMethod("functionLoad", String.class); + assertEquals(String.class, functionLoad.getReturnType()); + + // Test functionLoadReplace(String) exists + Method functionLoadReplace = jedisClass.getMethod("functionLoadReplace", String.class); + assertEquals(String.class, functionLoadReplace.getReturnType()); + + // Test functionDelete(String) exists + Method functionDelete = jedisClass.getMethod("functionDelete", String.class); + assertEquals(String.class, functionDelete.getReturnType()); + + // Test functionDump() exists + Method functionDump = jedisClass.getMethod("functionDump"); + assertEquals(byte[].class, functionDump.getReturnType()); + + // Test functionRestore(byte[]) exists + Method functionRestore = jedisClass.getMethod("functionRestore", byte[].class); + assertEquals(String.class, functionRestore.getReturnType()); + + // Test functionRestore(byte[], FunctionRestorePolicy) exists + Method functionRestoreWithPolicy = + jedisClass.getMethod( + "functionRestore", byte[].class, redis.clients.jedis.args.FunctionRestorePolicy.class); + assertEquals(String.class, functionRestoreWithPolicy.getReturnType()); + + // Test functionFlush() exists + Method functionFlush = jedisClass.getMethod("functionFlush"); + assertEquals(String.class, functionFlush.getReturnType()); + + // Test functionFlush(FlushMode) exists + Method functionFlushWithMode = + jedisClass.getMethod("functionFlush", redis.clients.jedis.args.FlushMode.class); + assertEquals(String.class, functionFlushWithMode.getReturnType()); + + // Test functionKill() exists + Method functionKill = jedisClass.getMethod("functionKill"); + assertEquals(String.class, functionKill.getReturnType()); + } + + @Test + public void testFunctionListMethodSignatures() throws NoSuchMethodException { + Class jedisClass = Jedis.class; + + // Test functionList() exists + Method functionList = jedisClass.getMethod("functionList"); + assertEquals(List.class, functionList.getReturnType()); + + // Test functionList(String) exists + Method functionListWithPattern = jedisClass.getMethod("functionList", String.class); + assertEquals(List.class, functionListWithPattern.getReturnType()); + + // Test functionListWithCode() exists + Method functionListWithCode = jedisClass.getMethod("functionListWithCode"); + assertEquals(List.class, functionListWithCode.getReturnType()); + + // Test functionListWithCode(String) exists + Method functionListWithCodeAndPattern = + jedisClass.getMethod("functionListWithCode", String.class); + assertEquals(List.class, functionListWithCodeAndPattern.getReturnType()); + + // Test functionStats() exists + Method functionStats = jedisClass.getMethod("functionStats"); + assertEquals(FunctionStats.class, functionStats.getReturnType()); + } + @Test public void testAclMethodSignatures() throws NoSuchMethodException { Class jedisClass = Jedis.class;