diff --git a/glide-core/src/protobuf/command_request.proto b/glide-core/src/protobuf/command_request.proto index f1e8f71add8..acd6ef8e7c6 100644 --- a/glide-core/src/protobuf/command_request.proto +++ b/glide-core/src/protobuf/command_request.proto @@ -256,6 +256,7 @@ enum RequestType { ScriptExists = 215; ScriptFlush = 216; ScriptKill = 217; + ScriptShow = 218; } message Command { diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index 3a1a25631fe..4350403b580 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -226,6 +226,7 @@ pub enum RequestType { ScriptExists = 215, ScriptFlush = 216, ScriptKill = 217, + ScriptShow = 218, } fn get_two_word_command(first: &str, second: &str) -> Cmd { @@ -455,6 +456,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::ScriptExists => RequestType::ScriptExists, ProtobufRequestType::ScriptFlush => RequestType::ScriptFlush, ProtobufRequestType::ScriptKill => RequestType::ScriptKill, + ProtobufRequestType::ScriptShow => RequestType::ScriptShow, } } } @@ -679,6 +681,7 @@ impl RequestType { RequestType::PubSubNumPat => Some(get_two_word_command("PUBSUB", "NUMPAT")), RequestType::PubSubSChannels => Some(get_two_word_command("PUBSUB", "SHARDCHANNELS")), RequestType::PubSubSNumSub => Some(get_two_word_command("PUBSUB", "SHARDNUMSUB")), + RequestType::ScriptShow => Some(get_two_word_command("SCRIPT", "SHOW")), RequestType::ScriptExists => Some(get_two_word_command("SCRIPT", "EXISTS")), RequestType::ScriptFlush => Some(get_two_word_command("SCRIPT", "FLUSH")), RequestType::ScriptKill => Some(get_two_word_command("SCRIPT", "KILL")), diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index a6294e1c0c0..d25a09ee605 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -110,6 +110,7 @@ import static command_request.CommandRequestOuterClass.RequestType.SScan; import static command_request.CommandRequestOuterClass.RequestType.SUnion; import static command_request.CommandRequestOuterClass.RequestType.SUnionStore; +import static command_request.CommandRequestOuterClass.RequestType.ScriptShow; import static command_request.CommandRequestOuterClass.RequestType.Set; import static command_request.CommandRequestOuterClass.RequestType.SetBit; import static command_request.CommandRequestOuterClass.RequestType.SetRange; @@ -1871,6 +1872,18 @@ public CompletableFuture invokeScript( } } + @Override + public CompletableFuture scriptShow(@NonNull String sha1) { + return commandManager.submitNewCommand( + ScriptShow, new String[] {sha1}, this::handleStringResponse); + } + + @Override + public CompletableFuture scriptShow(@NonNull GlideString sha1) { + return commandManager.submitNewCommand( + ScriptShow, new GlideString[] {sha1}, this::handleGlideStringResponse); + } + @Override public CompletableFuture zadd( @NonNull String key, diff --git a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java index fbc370a2768..798dbedf30d 100644 --- a/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ScriptingAndFunctionsBaseCommands.java @@ -128,4 +128,36 @@ CompletableFuture fcall( */ CompletableFuture fcallReadOnly( GlideString function, GlideString[] keys, GlideString[] arguments); + + /** + * Returns the original source code of a script in the script cache. + * + * @see valkey.io for details. + * @since Valkey 8.0.0 and above. + * @param sha1 The SHA1 digest of the script. + * @return The original source code of the script, if present in the cache. If the script is not + * found in the cache, an error is thrown. + * @example + *
{@code
+     * String scriptSource = client.scriptShow(luaScript.getHash()).get();
+     * assert scriptSource.equals("return { KEYS[1], ARGV[1] }");
+     * }
+ */ + CompletableFuture scriptShow(String sha1); + + /** + * Returns the original source code of a script in the script cache. + * + * @see valkey.io for details. + * @since Valkey 8.0.0 and above. + * @param sha1 The SHA1 digest of the script. + * @return The original source code of the script, if present in the cache. If the script is not + * found in the cache, an error is thrown. + * @example + *
{@code
+     * String scriptSource = client.scriptShow(gs(luaScript.getHash())).get();
+     * assert scriptSource.equals(gs("return { KEYS[1], ARGV[1] }"));
+     * }
+ */ + CompletableFuture scriptShow(GlideString sha1); } diff --git a/java/client/src/test/java/glide/api/GlideClientTest.java b/java/client/src/test/java/glide/api/GlideClientTest.java index 284d5c356d4..e1e6a530a2b 100644 --- a/java/client/src/test/java/glide/api/GlideClientTest.java +++ b/java/client/src/test/java/glide/api/GlideClientTest.java @@ -136,6 +136,7 @@ import static command_request.CommandRequestOuterClass.RequestType.SUnion; import static command_request.CommandRequestOuterClass.RequestType.SUnionStore; import static command_request.CommandRequestOuterClass.RequestType.Scan; +import static command_request.CommandRequestOuterClass.RequestType.ScriptShow; import static command_request.CommandRequestOuterClass.RequestType.Select; import static command_request.CommandRequestOuterClass.RequestType.SetBit; import static command_request.CommandRequestOuterClass.RequestType.SetRange; @@ -1581,6 +1582,28 @@ public void invokeScript_with_ScriptOptionsGlideString_returns_success() { assertEquals(payload, response.get()); } + @SneakyThrows + @Test + public void scriptShow_returns_script_source_glidestring() { + // setup + GlideString scriptSource = gs("return { KEYS[1], ARGV[1] }"); + GlideString hash = gs(UUID.randomUUID().toString()); + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(scriptSource); + + when(commandManager.submitNewCommand( + eq(ScriptShow), eq(new GlideString[] {hash}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.scriptShow(hash); + + // verify + assertEquals(testResponse, response); + assertEquals(scriptSource, response.get()); + } + @SneakyThrows @Test public void pttl_returns_success() { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index a7f48313b6a..7f6fd688083 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -3244,6 +3244,33 @@ public void invokeScript_gs_test(BaseClient client) { } } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void scriptShow_test(BaseClient client) { + assumeTrue(SERVER_VERSION.isGreaterThanOrEqualTo("7.9.0")); + + String code = "return '" + UUID.randomUUID().toString().substring(0, 5) + "'"; + Script script = new Script(code, false); + + // Load the script + client.invokeScript(script).get(); + + // Get the SHA1 digest of the script + String sha1 = script.getHash(); + + // Test with String + assertEquals(code, client.scriptShow(sha1).get()); + + // Test with GlideString + assertEquals(gs(code), client.scriptShow(gs(sha1)).get()); + + // Test with non-existing SHA1 + String nonExistingSha1 = UUID.randomUUID().toString(); + assertThrows(ExecutionException.class, () -> client.scriptShow(nonExistingSha1).get()); + assertThrows(ExecutionException.class, () -> client.scriptShow(gs(nonExistingSha1)).get()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1dfa5066d23..744469a03b2 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -165,6 +165,7 @@ import { createSScan, createSUnion, createSUnionStore, + createScriptShow, createSet, createSetBit, createSetRange, @@ -231,9 +232,9 @@ import { ConfigurationError, ConnectionError, ExecAbortError, - ValkeyError, RequestError, TimeoutError, + ValkeyError, } from "./Errors"; import { GlideClientConfiguration } from "./GlideClient"; import { @@ -3695,6 +3696,31 @@ export class BaseClient { return this.createWritePromise(scriptInvocation, options); } + /** + * Returns the original source code of a script in the script cache. + * + * @see {@link https://valkey.io/commands/script-show|valkey.io} for more details. + * @remarks Since Valkey version 8.0.0. + * + * @param sha1 - The SHA1 digest of the script. + * @param options - (Optional) See {@link DecoderOption}. + * @return The original source code of the script, if present in the cache. + * If the script is not found in the cache, an error is thrown. + * + * @example + * ```typescript + * const scriptHash = script.getHash(); + * const scriptSource = await client.scriptShow(scriptHash); + * console.log(scriptSource); // Output: "return { KEYS[1], ARGV[1] }" + * ``` + */ + public async scriptShow( + sha1: GlideString, + options?: DecoderOption, + ): Promise { + return this.createWritePromise(createScriptShow(sha1), options); + } + /** * Returns stream entries matching a given range of entry IDs. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 8190ae3cf4b..93c7df6a37d 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -3998,6 +3998,13 @@ export function createBZPopMin( return createCommand(RequestType.BZPopMin, [...keys, timeout.toString()]); } +/** + * @internal + */ +export function createScriptShow(sha1: GlideString): command_request.Command { + return createCommand(RequestType.ScriptShow, [sha1]); +} + /** * Time unit representation which is used in optional arguments for {@link BaseClient.getex|getex} and {@link BaseClient.set|set} command. */ diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index c663f9cbee8..4159e124f82 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -8,7 +8,7 @@ import { BaseClientConfiguration, Decoder, DecoderOption, - GlideRecord, // eslint-disable-line @typescript-eslint/no-unused-vars + GlideRecord, GlideReturnType, GlideString, PubSubMsg, diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index f952e496437..7f320165ce3 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -4192,6 +4192,33 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `script show test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("7.9.0")) { + return; + } + + const value = uuidv4(); + const code = `return '${value}'`; + const script = new Script(Buffer.from(code)); + + expect(await client.invokeScript(script)).toEqual(value); + + // Get the SHA1 digests of the script + const sha1 = script.getHash(); + + expect(await client.scriptShow(sha1)).toEqual(code); + + await expect( + client.scriptShow("non existing sha1"), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `zadd and zaddIncr test_%p`, async (protocol) => { diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 40e51f51ed8..8498b0ae44b 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -5393,6 +5393,27 @@ async def zintercard( await self._execute_command(RequestType.ZInterCard, args), ) + async def script_show(self, sha1: TEncodable) -> bytes: + """ + Returns the original source code of a script in the script cache. + + See https://valkey.io/commands/script-show for more details. + + Args: + sha1 (TEncodable): The SHA1 digest of the script. + + Returns: + bytes: The original source code of the script, if present in the cache. + If the script is not found in the cache, an error is thrown. + + Example: + >>> await client.script_show(script.get_hash()) + b"return { KEYS[1], ARGV[1] }" + + Since: Valkey version 8.0.0. + """ + return cast(bytes, await self._execute_command(RequestType.ScriptShow, [sha1])) + async def pfadd(self, key: TEncodable, elements: List[TEncodable]) -> int: """ Adds all elements to the HyperLogLog data structure stored at the specified `key`. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 47655d55267..2991e0ab2ce 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -10477,3 +10477,24 @@ async def attempt_kill_writing_script(): await test_client.close() await test_client2.close() + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_script_show(self, glide_client: TGlideClient): + min_version = "7.9.0" + if await check_if_server_version_lt(glide_client, min_version): + return pytest.mark.skip(reason=f"Valkey version required >= {min_version}") + + code = f"return '{get_random_string(5)}'" + script = Script(code) + + # Load the scripts + await glide_client.invoke_script(script) + + # Get the SHA1 digests of the script + sha1 = script.get_hash() + + assert await glide_client.script_show(sha1) == code.encode() + + with pytest.raises(RequestError): + await glide_client.script_show("non existing sha1")