Skip to main content

glide/
script.rs

1// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2//! Lua script helper (`Script`): SHA-caching `EVALSHA` with `EVAL` fallback.
3//!
4//!
5//! ```rust,no_run
6//! use glide::Script;
7//! # async fn demo(mut client: glide::GlideClient) -> glide::ValkeyResult<()> {
8//! let script = Script::new("return tonumber(ARGV[1]) + tonumber(ARGV[2])");
9//! let sum: i64 = script.arg(1).arg(2).invoke_async(&mut client).await?;
10//! assert_eq!(sum, 3);
11//! # Ok(()) }
12//! ```
13//!
14//! Async methods take an async GLIDE client ([`crate::GlideClient`] or
15//! [`crate::GlideClusterClient`]); blocking methods take a sync GLIDE client
16//! ([`crate::sync::SyncGlideClient`] or [`crate::sync::SyncGlideClusterClient`]).
17
18use crate::ValkeyFuture;
19use crate::ValkeyResult;
20use crate::cmd::cmd;
21use crate::commands::core::AsyncCommands;
22use crate::value::FromValkeyValue;
23use crate::value::ValkeyValue;
24use crate::write::ToValkeyArgs;
25use glide_core::scripts_container::add_script;
26use glide_core::scripts_container::remove_script;
27
28/// Runs a cached script by hash on an async client.
29/// Implemented by [`GlideClient`] and [`GlideClusterClient`].
30#[doc(hidden)]
31#[sealed::sealed(pub(crate))]
32pub trait ScriptInvoke {
33    fn glide_invoke_script<'a>(
34        &'a self,
35        hash: &'a str,
36        keys: &'a [Vec<u8>],
37        args: &'a [Vec<u8>],
38    ) -> ValkeyFuture<'a, ValkeyValue>;
39}
40
41/// Runs a cached script by hash on a sync client.
42/// Blocking counterpart of [`ScriptInvoke`].
43/// Implemented by the [`SyncGlideClient`] and [`SyncGlideClusterClient`].
44#[cfg(feature = "sync")]
45#[doc(hidden)]
46#[sealed::sealed(pub(crate))]
47pub trait ScriptInvokeSync {
48    fn glide_invoke_script(
49        &self,
50        hash: &str,
51        keys: &[Vec<u8>],
52        args: &[Vec<u8>],
53    ) -> ValkeyResult<ValkeyValue>;
54}
55
56/// A cached Lua script with its SHA-1 hash.
57///
58/// Create once (computes the SHA-1), then [`Self::arg`]/[`Self::key`] to build
59/// an invocation. See the [module docs](self) for an example.
60#[derive(Debug)]
61pub struct Script {
62    code: String,
63    hash: String,
64}
65
66impl Script {
67    /// Create a new script object with a precomputed SHA-1 hash.
68    pub fn new(code: &str) -> Script {
69        let hash = add_script(code.as_bytes());
70        Script {
71            code: code.to_string(),
72            hash,
73        }
74    }
75
76    /// The SHA-1 hash of the script, as used by `EVALSHA`.
77    pub fn get_hash(&self) -> &str {
78        &self.hash
79    }
80
81    /// Create an invocation and add a regular argument (`ARGV[…]`).
82    #[must_use]
83    pub fn arg<'a, T: ToValkeyArgs>(&'a self, arg: T) -> ScriptInvocation<'a> {
84        let mut invocation = self.prepare_invoke();
85        invocation.arg(arg);
86        invocation
87    }
88
89    /// Create an invocation and add a key argument (`KEYS[…]`).
90    #[must_use]
91    pub fn key<'a, T: ToValkeyArgs>(&'a self, key: T) -> ScriptInvocation<'a> {
92        let mut invocation = self.prepare_invoke();
93        invocation.key(key);
94        invocation
95    }
96
97    /// Create an empty invocation (no keys, no args).
98    #[must_use]
99    pub fn prepare_invoke(&self) -> ScriptInvocation<'_> {
100        ScriptInvocation {
101            script: self,
102            args: Vec::new(),
103            keys: Vec::new(),
104        }
105    }
106
107    /// Invoke the script without keys or args.
108    pub async fn invoke_async<C: ScriptInvoke, T: FromValkeyValue>(
109        &self,
110        con: &C,
111    ) -> ValkeyResult<T> {
112        self.prepare_invoke().invoke_async(con).await
113    }
114
115    /// Invoke the script without keys or args on a **blocking** connection
116    /// ([`crate::sync::SyncGlideClient`] / [`crate::sync::SyncGlideClusterClient`]).
117    #[cfg(feature = "sync")]
118    pub fn invoke<C: ScriptInvokeSync, T: FromValkeyValue>(&self, con: &C) -> ValkeyResult<T> {
119        self.prepare_invoke().invoke(con)
120    }
121
122    /// Load the script into the server's script cache (`SCRIPT LOAD`) without
123    /// running it; returns the SHA-1 hash.
124    pub async fn load_async<C: AsyncCommands>(&self, con: &C) -> ValkeyResult<String> {
125        let mut load = cmd("SCRIPT");
126        load.arg("LOAD").arg(self.code.as_bytes());
127        String::from_owned_valkey_value(con.glide_send_command(load).await?)
128    }
129
130    /// Load the script into the server's script cache (`SCRIPT LOAD`) on a
131    /// **blocking** connection; returns the SHA-1 hash.
132    #[cfg(feature = "sync")]
133    pub fn load<C: crate::commands::core::Commands>(&self, con: &C) -> ValkeyResult<String> {
134        let mut load = cmd("SCRIPT");
135        load.arg("LOAD").arg(self.code.as_bytes());
136        String::from_owned_valkey_value(con.glide_send_command(load)?)
137    }
138}
139
140impl Clone for Script {
141    fn clone(&self) -> Self {
142        // Bump glide-core's ref-count for the script.
143        add_script(self.code.as_bytes());
144        Script {
145            code: self.code.clone(),
146            hash: self.hash.clone(),
147        }
148    }
149}
150
151impl Drop for Script {
152    fn drop(&mut self) {
153        remove_script(&self.hash);
154    }
155}
156
157/// A pending script invocation: keys + args bound to a [`Script`].
158#[derive(Debug, Clone)]
159pub struct ScriptInvocation<'a> {
160    script: &'a Script,
161    args: Vec<Vec<u8>>,
162    keys: Vec<Vec<u8>>,
163}
164
165impl ScriptInvocation<'_> {
166    /// Add a regular argument (`ARGV[…]`). Builder form.
167    pub fn arg<T: ToValkeyArgs>(&mut self, arg: T) -> &mut Self {
168        arg.write_valkey_args(&mut self.args);
169        self
170    }
171
172    /// Add a key argument (`KEYS[…]`). Builder form.
173    pub fn key<T: ToValkeyArgs>(&mut self, key: T) -> &mut Self {
174        key.write_valkey_args(&mut self.keys);
175        self
176    }
177
178    /// Executes the script using [`EVALSHA`], with automatic fallback to
179    /// [`EVAL`] if the script is not cached on the server.
180    ///
181    /// [`EVALSHA`]: https://valkey.io/commands/evalsha/
182    /// [`EVAL`]: https://valkey.io/commands/eval/
183    pub async fn invoke_async<C: ScriptInvoke, T: FromValkeyValue>(
184        &self,
185        con: &C,
186    ) -> ValkeyResult<T> {
187        let value = con
188            .glide_invoke_script(&self.script.hash, &self.keys, &self.args)
189            .await?;
190        T::from_owned_valkey_value(value)
191    }
192
193    /// Executes the script on a **blocking** connection using [`EVALSHA`],
194    /// with automatic fallback to [`EVAL`] if the script is not cached on the server.
195    ///
196    /// [`EVALSHA`]: https://valkey.io/commands/evalsha/
197    /// [`EVAL`]: https://valkey.io/commands/eval/
198    #[cfg(feature = "sync")]
199    pub fn invoke<C: ScriptInvokeSync, T: FromValkeyValue>(&self, con: &C) -> ValkeyResult<T> {
200        let value = con.glide_invoke_script(&self.script.hash, &self.keys, &self.args)?;
201        T::from_owned_valkey_value(value)
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn sha1_matches_server_semantics() {
211        // SHA-1 of "return 1" (verified via `printf 'return 1' | sha1sum` and
212        // matching the server's SCRIPT LOAD result).
213        let script = Script::new("return 1");
214        assert_eq!(
215            script.get_hash(),
216            "e0e1f9fabfc9d4800c877a703b823ac0578ff8db"
217        );
218    }
219
220    #[test]
221    fn invocation_collects_keys_and_args() {
222        let script = Script::new("return KEYS[1]");
223        let mut invocation = script.prepare_invoke();
224        invocation.key("k1").key("k2").arg("a1").arg(2);
225        assert_eq!(invocation.keys, vec![b"k1".to_vec(), b"k2".to_vec()]);
226        assert_eq!(invocation.args, vec![b"a1".to_vec(), b"2".to_vec()]);
227    }
228}