Module: Valkey::Commands::ScriptingCommands

Included in:
Valkey::Commands
Defined in:
lib/valkey/commands/scripting_commands.rb

Overview

this module contains commands related to list data type.

Instance Method Summary collapse

Instance Method Details

#eval(script, *rest, keys: nil, args: nil) ⇒ Object

Execute a Lua script on the server.

Since the eval is not available in the rust backend using the load and invoke script

Examples:

Execute a simple script

valkey.eval("return 1")
  # => 1

Execute script with keys and arguments

valkey.eval("return KEYS[1] .. ARGV[1]", keys: ["mykey"], args: ["myarg"])
  # => "mykeynyarg"

Execute script with multiple keys and arguments

valkey.eval("return #KEYS + #ARGV", keys: ["key1", "key2"], args: ["arg1", "arg2", "arg3"])
  # => 5

Execute script that returns different data types

valkey.eval("return {1, 'hello', true, nil}")
  # => [1, "hello", true, nil]

Positional form, matching redis-rb's eval(script, keys, argv)

valkey.eval("return KEYS[1] .. ARGV[1]", ["mykey"], ["myarg"])
  # => "mykeynyarg"

Parameters:

  • script (String)

    the Lua script to execute

  • keys (Array<String>) (defaults to: nil)

    array of key names that the script will access

  • args (Array<Object>) (defaults to: nil)

    array of arguments to pass to the script

Returns:

  • (Object)

    the result of the script execution

Raises:

  • (ArgumentError)

    if script is empty

  • (CommandError)

    if script execution fails



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/valkey/commands/scripting_commands.rb', line 145

def eval(script, *rest, keys: nil, args: nil)
  # Validate script parameter
  raise ArgumentError, "script must be a string" unless script.is_a?(String)
  raise ArgumentError, "script cannot be empty" if script.empty?

  # Accept redis-rb's flexible positional form - eval(script, keys, argv)
  # - in addition to the keyword form, mirroring the same treatment
  # applied to evalsha (see that method for the full rationale).
  keys ||= rest[0] || []
  args ||= rest[1] || []

  # Validate and convert keys and args to strings
  begin
    keys = Array(keys).map(&:to_s)
    args = Array(args).map(&:to_s)
  rescue StandardError => e
    raise ArgumentError, "failed to convert keys or args to strings: #{e.message}"
  end

  # Load script to get SHA1 hash, then execute via invoke_script
  sha = script_load(script)
  invoke_script(sha, keys: keys, args: args)
end

#eval_ro(script, keys: [], args: []) ⇒ Object

Execute a read-only Lua script on the server.

This is a read-only variant of EVAL that cannot execute commands that modify data. It can be routed to read replicas.

Examples:

Execute a read-only script

valkey.eval_ro("return redis.call('get', KEYS[1])", keys: ["mykey"])
  # => "myvalue"

Parameters:

  • script (String)

    the Lua script to execute

  • keys (Array<String>) (defaults to: [])

    array of key names that the script will access

  • args (Array<Object>) (defaults to: [])

    array of arguments to pass to the script

Returns:

  • (Object)

    the result of the script execution

Raises:

  • (ArgumentError)

See Also:



236
237
238
239
240
241
242
243
244
245
# File 'lib/valkey/commands/scripting_commands.rb', line 236

def eval_ro(script, keys: [], args: [])
  raise ArgumentError, "script must be a string" unless script.is_a?(String)
  raise ArgumentError, "script cannot be empty" if script.empty?

  keys = Array(keys).map(&:to_s)
  args = Array(args).map(&:to_s)

  sha = script_load(script)
  invoke_script(sha, keys: keys, args: args)
end

#evalsha(sha, *rest, keys: nil, args: nil) ⇒ Object

Execute a cached Lua script by its SHA1 hash.

Since evalsha is not available in rust backend using invoke script

Examples:

Execute a cached script

sha = valkey.script_load("return 1")
valkey.evalsha(sha)
  # => 1

Execute cached script with parameters

script = "return KEYS[1] .. ':' .. ARGV[1]"
sha = valkey.script_load(script)
valkey.evalsha(sha, keys: ["user"], args: ["123"])
  # => "user:123"

Handle script not found error

begin
  valkey.evalsha("nonexistent_sha", keys: [], args: [])
rescue Valkey::CommandError => e
  puts "Script not found: #{e.message}"
end

Positional form, matching redis-rb's evalsha(sha, keys, argv)

valkey.evalsha(sha, ["user"], ["123"])
  # => "user:123"

Parameters:

  • sha (String)

    the SHA1 hash of the script to execute

  • keys (Array<String>) (defaults to: nil)

    array of key names that the script will access

  • args (Array<Object>) (defaults to: nil)

    array of arguments to pass to the script

Returns:

  • (Object)

    the result of the script execution

Raises:

  • (ArgumentError)

    if SHA1 hash format is invalid

  • (CommandError)

    if script is not found or execution fails



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/valkey/commands/scripting_commands.rb', line 198

def evalsha(sha, *rest, keys: nil, args: nil)
  # Validate SHA1 hash parameter
  raise ArgumentError, "sha1 hash must be a string" unless sha.is_a?(String)
  raise ArgumentError, "sha1 hash must be a 40-character hexadecimal string" unless valid_sha1?(sha)

  # Accept redis-rb's flexible positional form - evalsha(sha, keys, argv)
  # - in addition to the keyword form, so code written against redis-rb's
  # API (e.g. redis_display_id) doesn't need to special-case this client.
  keys ||= rest[0] || []
  args ||= rest[1] || []

  # Validate and convert keys and args to strings
  begin
    keys = Array(keys).map(&:to_s)
    args = Array(args).map(&:to_s)
  rescue StandardError => e
    raise ArgumentError, "failed to convert keys or args to strings: #{e.message}"
  end

  # Execute cached script via invoke_script
  invoke_script(sha, keys: keys, args: args)
end

#evalsha_ro(sha, keys: [], args: []) ⇒ Object

Execute a cached read-only Lua script by its SHA1 hash.

This is a read-only variant of EVALSHA that cannot execute commands that modify data. It can be routed to read replicas.

Examples:

Execute a cached read-only script

sha = valkey.script_load("return redis.call('get', KEYS[1])")
valkey.evalsha_ro(sha, keys: ["mykey"])
  # => "myvalue"

Parameters:

  • sha (String)

    the SHA1 hash of the script to execute

  • keys (Array<String>) (defaults to: [])

    array of key names that the script will access

  • args (Array<Object>) (defaults to: [])

    array of arguments to pass to the script

Returns:

  • (Object)

    the result of the script execution

Raises:

  • (ArgumentError)

See Also:



263
264
265
266
267
268
269
270
271
# File 'lib/valkey/commands/scripting_commands.rb', line 263

def evalsha_ro(sha, keys: [], args: [])
  raise ArgumentError, "sha1 hash must be a string" unless sha.is_a?(String)
  raise ArgumentError, "sha1 hash must be a 40-character hexadecimal string" unless valid_sha1?(sha)

  keys = Array(keys).map(&:to_s)
  args = Array(args).map(&:to_s)

  invoke_script(sha, keys: keys, args: args)
end

#invoke_script(script, args: [], keys: []) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/valkey/commands/scripting_commands.rb', line 273

def invoke_script(script, args: [], keys: [])
  # Must hold onto the returned buffers (_arg_bufs/_keys_bufs) for the
  # lifetime of this method - they back arg_ptrs/keys_ptrs, and letting
  # them go out of scope (e.g. by only capturing the first 2 return
  # values) makes them eligible for GC before the native call below
  # reads through those pointers, corrupting ARGV/KEYS with freed memory.
  arg_ptrs, arg_lens, _arg_bufs, flattened_args = build_command_args(args)
  keys_ptrs, keys_lens, _keys_bufs, flattened_keys = build_command_args(keys)

  route = ""
  route_buf = FFI::MemoryPointer.from_string(route)

  # Use from_string to ensure proper null termination
  sha = FFI::MemoryPointer.from_string(script)

  begin
    res = Bindings.invoke_script(
      @connection,
      0,
      sha,
      flattened_keys.size,
      keys_ptrs,
      keys_lens,
      flattened_args.size,
      arg_ptrs,
      arg_lens,
      route_buf,
      route.bytesize,
      0 # span_ptr for OpenTelemetry (0 = no span)
    )

    convert_response(res)
  ensure
    Bindings.free_command_result(res) if res && !res.null?
  end
end

#script(subcommand, args = nil, options: {}) ⇒ String, ...

Control remote script registry.

Examples:

Load a script

sha = valkey.script(:load, "return 1")
  # => <sha of this script>

Check if a script exists

valkey.script(:exists, sha)
  # => true

Check if multiple scripts exist

valkey.script(:exists, [sha, other_sha])
  # => [true, false]

Flush the script registry

valkey.script(:flush)
  # => "OK"

Kill a running script

valkey.script(:kill)
  # => "OK"

Parameters:

  • subcommand (String)

    e.g. exists, flush, load, kill

  • args (Array<String>) (defaults to: nil)

    depends on subcommand

Returns:

  • (String, Boolean, Array<Boolean>, ...)

    depends on subcommand

See Also:



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/valkey/commands/scripting_commands.rb', line 34

def script(subcommand, args = nil, options: {})
  subcommand = subcommand.to_s.downcase

  if args.nil?
    send("script_#{subcommand}", **options)
  else
    send("script_#{subcommand}", args)
  end

  # if subcommand == "exists"
  #   arg = args.first
  #
  #   send_command([:script, :exists, arg]) do |reply|
  #     reply = reply.map { |r| Boolify.call(r) }
  #
  #     if arg.is_a?(Array)
  #       reply
  #     else
  #       reply.first
  #     end
  #   end
  # else
  #   send_command([:script, subcommand] + args)
  # end
end

#script_debug(mode) ⇒ String

Set the debug mode for subsequent scripts executed with EVAL.

Examples:

Enable script debugging

valkey.script_debug("YES")
  # => "OK"

Disable script debugging

valkey.script_debug("NO")
  # => "OK"

Parameters:

  • mode (String)

    debug mode: "YES", "SYNC", or "NO"

Returns:

  • (String)

    "OK"

See Also:



99
100
101
# File 'lib/valkey/commands/scripting_commands.rb', line 99

def script_debug(mode)
  send_command(RequestType::SCRIPT_DEBUG, [mode.to_s.upcase])
end

#script_exists(args) ⇒ Object



72
73
74
75
76
77
78
79
80
# File 'lib/valkey/commands/scripting_commands.rb', line 72

def script_exists(args)
  send_command(RequestType::SCRIPT_EXISTS, Array(args)) do |reply|
    if args.is_a?(Array)
      reply
    else
      reply.first
    end
  end
end

#script_flush(sync: false, async: false) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
# File 'lib/valkey/commands/scripting_commands.rb', line 60

def script_flush(sync: false, async: false)
  args = []

  if async
    args << "async"
  elsif sync
    args << "sync"
  end

  send_command(RequestType::SCRIPT_FLUSH, args)
end

#script_killObject



82
83
84
# File 'lib/valkey/commands/scripting_commands.rb', line 82

def script_kill
  send_command(RequestType::SCRIPT_KILL)
end

#script_load(script) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/valkey/commands/scripting_commands.rb', line 103

def script_load(script)
  script = script.first if script.is_a?(Array)

  buf = FFI::MemoryPointer.new(:char, script.bytesize)
  buf.put_bytes(0, script)

  result = Bindings.store_script(buf, script.bytesize)

  begin
    hash_buffer = Bindings::ScriptHashBuffer.new(result)
    hash_buffer[:ptr].read_string(hash_buffer[:len])
  ensure
    Bindings.free_script_hash_buffer(result) if result && !result.null?
  end
end