valkey-rb 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +58 -0
  3. data/.rubocop_todo.yml +22 -0
  4. data/README.md +95 -0
  5. data/Rakefile +23 -0
  6. data/lib/valkey/bindings.rb +224 -0
  7. data/lib/valkey/commands/bitmap_commands.rb +86 -0
  8. data/lib/valkey/commands/cluster_commands.rb +259 -0
  9. data/lib/valkey/commands/connection_commands.rb +318 -0
  10. data/lib/valkey/commands/function_commands.rb +255 -0
  11. data/lib/valkey/commands/generic_commands.rb +525 -0
  12. data/lib/valkey/commands/geo_commands.rb +87 -0
  13. data/lib/valkey/commands/hash_commands.rb +587 -0
  14. data/lib/valkey/commands/hyper_log_log_commands.rb +51 -0
  15. data/lib/valkey/commands/json_commands.rb +389 -0
  16. data/lib/valkey/commands/list_commands.rb +348 -0
  17. data/lib/valkey/commands/module_commands.rb +125 -0
  18. data/lib/valkey/commands/pubsub_commands.rb +237 -0
  19. data/lib/valkey/commands/scripting_commands.rb +286 -0
  20. data/lib/valkey/commands/server_commands.rb +961 -0
  21. data/lib/valkey/commands/set_commands.rb +220 -0
  22. data/lib/valkey/commands/sorted_set_commands.rb +971 -0
  23. data/lib/valkey/commands/stream_commands.rb +636 -0
  24. data/lib/valkey/commands/string_commands.rb +359 -0
  25. data/lib/valkey/commands/transaction_commands.rb +175 -0
  26. data/lib/valkey/commands/vector_search_commands.rb +271 -0
  27. data/lib/valkey/commands.rb +68 -0
  28. data/lib/valkey/errors.rb +41 -0
  29. data/lib/valkey/libglide_ffi.so +0 -0
  30. data/lib/valkey/opentelemetry.rb +207 -0
  31. data/lib/valkey/pipeline.rb +20 -0
  32. data/lib/valkey/protobuf/command_request_pb.rb +51 -0
  33. data/lib/valkey/protobuf/connection_request_pb.rb +51 -0
  34. data/lib/valkey/protobuf/response_pb.rb +39 -0
  35. data/lib/valkey/pubsub_callback.rb +10 -0
  36. data/lib/valkey/request_error_type.rb +10 -0
  37. data/lib/valkey/request_type.rb +436 -0
  38. data/lib/valkey/response_type.rb +20 -0
  39. data/lib/valkey/utils.rb +253 -0
  40. data/lib/valkey/version.rb +5 -0
  41. data/lib/valkey.rb +551 -0
  42. metadata +119 -0
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ module Commands
5
+ # This module contains commands related to Valkey Pub/Sub.
6
+ #
7
+ # @see https://valkey.io/commands/#pubsub
8
+ #
9
+ module PubSubCommands
10
+ # Subscribe to one or more channels.
11
+ #
12
+ # @example Subscribe to channels
13
+ # valkey.subscribe("channel1", "channel2")
14
+ # # => "OK"
15
+ #
16
+ # @param [Array<String>] channels the channels to subscribe to
17
+ # @return [String] "OK"
18
+ #
19
+ # @see https://valkey.io/commands/subscribe/
20
+ def subscribe(*channels)
21
+ send_command(RequestType::SUBSCRIBE, channels)
22
+ end
23
+
24
+ # Unsubscribe from one or more channels.
25
+ #
26
+ # @example Unsubscribe from channels
27
+ # valkey.unsubscribe("channel1", "channel2")
28
+ # # => "OK"
29
+ # @example Unsubscribe from all channels
30
+ # valkey.unsubscribe
31
+ # # => "OK"
32
+ #
33
+ # @param [Array<String>] channels the channels to unsubscribe from (empty for all)
34
+ # @return [String] "OK"
35
+ #
36
+ # @see https://valkey.io/commands/unsubscribe/
37
+ def unsubscribe(*channels)
38
+ send_command(RequestType::UNSUBSCRIBE, channels)
39
+ end
40
+
41
+ # Subscribe to one or more patterns.
42
+ #
43
+ # @example Subscribe to patterns
44
+ # valkey.psubscribe("news.*", "events.*")
45
+ # # => "OK"
46
+ #
47
+ # @param [Array<String>] patterns the patterns to subscribe to
48
+ # @return [String] "OK"
49
+ #
50
+ # @see https://valkey.io/commands/psubscribe/
51
+ def psubscribe(*patterns)
52
+ send_command(RequestType::PSUBSCRIBE, patterns)
53
+ end
54
+
55
+ # Unsubscribe from one or more patterns.
56
+ #
57
+ # @example Unsubscribe from patterns
58
+ # valkey.punsubscribe("news.*", "events.*")
59
+ # # => "OK"
60
+ # @example Unsubscribe from all patterns
61
+ # valkey.punsubscribe
62
+ # # => "OK"
63
+ #
64
+ # @param [Array<String>] patterns the patterns to unsubscribe from (empty for all)
65
+ # @return [String] "OK"
66
+ #
67
+ # @see https://valkey.io/commands/punsubscribe/
68
+ def punsubscribe(*patterns)
69
+ send_command(RequestType::PUNSUBSCRIBE, patterns)
70
+ end
71
+
72
+ # Publish a message to a channel.
73
+ #
74
+ # @example Publish a message
75
+ # valkey.publish("channel1", "Hello, World!")
76
+ # # => 2
77
+ #
78
+ # @param [String] channel the channel to publish to
79
+ # @param [String] message the message to publish
80
+ # @return [Integer] the number of clients that received the message
81
+ #
82
+ # @see https://valkey.io/commands/publish/
83
+ def publish(channel, message)
84
+ send_command(RequestType::PUBLISH, [channel, message])
85
+ end
86
+
87
+ # Subscribe to one or more shard channels.
88
+ #
89
+ # @example Subscribe to shard channels
90
+ # valkey.ssubscribe("shard1", "shard2")
91
+ # # => "OK"
92
+ #
93
+ # @param [Array<String>] channels the shard channels to subscribe to
94
+ # @return [String] "OK"
95
+ #
96
+ # @see https://valkey.io/commands/ssubscribe/
97
+ def ssubscribe(*channels)
98
+ send_command(RequestType::SSUBSCRIBE, channels)
99
+ end
100
+
101
+ # Unsubscribe from one or more shard channels.
102
+ #
103
+ # @example Unsubscribe from shard channels
104
+ # valkey.sunsubscribe("shard1", "shard2")
105
+ # # => "OK"
106
+ # @example Unsubscribe from all shard channels
107
+ # valkey.sunsubscribe
108
+ # # => "OK"
109
+ #
110
+ # @param [Array<String>] channels the shard channels to unsubscribe from (empty for all)
111
+ # @return [String] "OK"
112
+ #
113
+ # @see https://valkey.io/commands/sunsubscribe/
114
+ def sunsubscribe(*channels)
115
+ send_command(RequestType::SUNSUBSCRIBE, channels)
116
+ end
117
+
118
+ # Publish a message to a shard channel.
119
+ #
120
+ # @example Publish a message to a shard channel
121
+ # valkey.spublish("shard1", "Hello, Shard!")
122
+ # # => 1
123
+ #
124
+ # @param [String] channel the shard channel to publish to
125
+ # @param [String] message the message to publish
126
+ # @return [Integer] the number of clients that received the message
127
+ #
128
+ # @see https://valkey.io/commands/spublish/
129
+ def spublish(channel, message)
130
+ send_command(RequestType::SPUBLISH, [channel, message])
131
+ end
132
+
133
+ # List active channels.
134
+ #
135
+ # @example List all active channels
136
+ # valkey.pubsub_channels
137
+ # # => ["channel1", "channel2"]
138
+ # @example List active channels matching a pattern
139
+ # valkey.pubsub_channels("news.*")
140
+ # # => ["news.sports", "news.tech"]
141
+ #
142
+ # @param [String] pattern optional pattern to filter channels
143
+ # @return [Array<String>] list of active channels
144
+ #
145
+ # @see https://valkey.io/commands/pubsub-channels/
146
+ def pubsub_channels(pattern = nil)
147
+ args = pattern ? [pattern] : []
148
+ send_command(RequestType::PUBSUB_CHANNELS, args)
149
+ end
150
+
151
+ # Get the number of unique patterns subscribed to.
152
+ #
153
+ # @example Get pattern count
154
+ # valkey.pubsub_numpat
155
+ # # => 3
156
+ #
157
+ # @return [Integer] the number of patterns
158
+ #
159
+ # @see https://valkey.io/commands/pubsub-numpat/
160
+ def pubsub_numpat
161
+ send_command(RequestType::PUBSUB_NUM_PAT)
162
+ end
163
+
164
+ # Get the number of subscribers for channels.
165
+ #
166
+ # @example Get subscriber counts
167
+ # valkey.pubsub_numsub("channel1", "channel2")
168
+ # # => ["channel1", 5, "channel2", 3]
169
+ #
170
+ # @param [Array<String>] channels the channels to check
171
+ # @return [Array] channel names and subscriber counts
172
+ #
173
+ # @see https://valkey.io/commands/pubsub-numsub/
174
+ def pubsub_numsub(*channels)
175
+ send_command(RequestType::PUBSUB_NUM_SUB, channels)
176
+ end
177
+
178
+ # List active shard channels.
179
+ #
180
+ # @example List all active shard channels
181
+ # valkey.pubsub_shardchannels
182
+ # # => ["shard1", "shard2"]
183
+ # @example List active shard channels matching a pattern
184
+ # valkey.pubsub_shardchannels("shard.*")
185
+ # # => ["shard.1", "shard.2"]
186
+ #
187
+ # @param [String] pattern optional pattern to filter shard channels
188
+ # @return [Array<String>] list of active shard channels
189
+ #
190
+ # @see https://valkey.io/commands/pubsub-shardchannels/
191
+ def pubsub_shardchannels(pattern = nil)
192
+ args = pattern ? [pattern] : []
193
+ send_command(RequestType::PUBSUB_SHARD_CHANNELS, args)
194
+ end
195
+
196
+ # Get the number of subscribers for shard channels.
197
+ #
198
+ # @example Get shard subscriber counts
199
+ # valkey.pubsub_shardnumsub("shard1", "shard2")
200
+ # # => ["shard1", 2, "shard2", 1]
201
+ #
202
+ # @param [Array<String>] channels the shard channels to check
203
+ # @return [Array] shard channel names and subscriber counts
204
+ #
205
+ # @see https://valkey.io/commands/pubsub-shardnumsub/
206
+ def pubsub_shardnumsub(*channels)
207
+ send_command(RequestType::PUBSUB_SHARD_NUM_SUB, channels)
208
+ end
209
+
210
+ # Control pub/sub operations (convenience method).
211
+ #
212
+ # @example List active channels
213
+ # valkey.pubsub(:channels)
214
+ # # => ["channel1", "channel2"]
215
+ # @example Get pattern count
216
+ # valkey.pubsub(:numpat)
217
+ # # => 3
218
+ # @example Get subscriber counts
219
+ # valkey.pubsub(:numsub, "channel1", "channel2")
220
+ # # => ["channel1", 5, "channel2", 3]
221
+ # @example List active shard channels
222
+ # valkey.pubsub(:shardchannels)
223
+ # # => ["shard1", "shard2"]
224
+ # @example Get shard subscriber counts
225
+ # valkey.pubsub(:shardnumsub, "shard1", "shard2")
226
+ # # => ["shard1", 2, "shard2", 1]
227
+ #
228
+ # @param [String, Symbol] subcommand the subcommand (channels, numpat, numsub, shardchannels, shardnumsub)
229
+ # @param [Array] args arguments for the subcommand
230
+ # @return [Object] depends on subcommand
231
+ def pubsub(subcommand, *args)
232
+ subcommand = subcommand.to_s.downcase
233
+ send("pubsub_#{subcommand}", *args)
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ module Commands
5
+ # this module contains commands related to list data type.
6
+ #
7
+ # @see https://valkey.io/commands/#scripting
8
+ #
9
+ module ScriptingCommands
10
+ # Control remote script registry.
11
+ #
12
+ # @example Load a script
13
+ # sha = valkey.script(:load, "return 1")
14
+ # # => <sha of this script>
15
+ # @example Check if a script exists
16
+ # valkey.script(:exists, sha)
17
+ # # => true
18
+ # @example Check if multiple scripts exist
19
+ # valkey.script(:exists, [sha, other_sha])
20
+ # # => [true, false]
21
+ # @example Flush the script registry
22
+ # valkey.script(:flush)
23
+ # # => "OK"
24
+ # @example Kill a running script
25
+ # valkey.script(:kill)
26
+ # # => "OK"
27
+ #
28
+ # @param [String] subcommand e.g. `exists`, `flush`, `load`, `kill`
29
+ # @param [Array<String>] args depends on subcommand
30
+ # @return [String, Boolean, Array<Boolean>, ...] depends on subcommand
31
+ #
32
+ # @see #eval
33
+ # @see #evalsha
34
+ def script(subcommand, args = nil, options: {})
35
+ subcommand = subcommand.to_s.downcase
36
+
37
+ if args.nil?
38
+ send("script_#{subcommand}", **options)
39
+ else
40
+ send("script_#{subcommand}", args)
41
+ end
42
+
43
+ # if subcommand == "exists"
44
+ # arg = args.first
45
+ #
46
+ # send_command([:script, :exists, arg]) do |reply|
47
+ # reply = reply.map { |r| Boolify.call(r) }
48
+ #
49
+ # if arg.is_a?(Array)
50
+ # reply
51
+ # else
52
+ # reply.first
53
+ # end
54
+ # end
55
+ # else
56
+ # send_command([:script, subcommand] + args)
57
+ # end
58
+ end
59
+
60
+ def script_flush(sync: false, async: false)
61
+ args = []
62
+
63
+ if async
64
+ args << "async"
65
+ elsif sync
66
+ args << "sync"
67
+ end
68
+
69
+ send_command(RequestType::SCRIPT_FLUSH, args)
70
+ end
71
+
72
+ def script_exists(args)
73
+ send_command(RequestType::SCRIPT_EXISTS, Array(args)) do |reply|
74
+ if args.is_a?(Array)
75
+ reply
76
+ else
77
+ reply.first
78
+ end
79
+ end
80
+ end
81
+
82
+ def script_kill
83
+ send_command(RequestType::SCRIPT_KILL)
84
+ end
85
+
86
+ # Set the debug mode for subsequent scripts executed with EVAL.
87
+ #
88
+ # @param [String] mode debug mode: "YES", "SYNC", or "NO"
89
+ # @return [String] "OK"
90
+ #
91
+ # @example Enable script debugging
92
+ # valkey.script_debug("YES")
93
+ # # => "OK"
94
+ # @example Disable script debugging
95
+ # valkey.script_debug("NO")
96
+ # # => "OK"
97
+ #
98
+ # @see https://valkey.io/commands/script-debug/
99
+ def script_debug(mode)
100
+ send_command(RequestType::SCRIPT_DEBUG, [mode.to_s.upcase])
101
+ end
102
+
103
+ def script_load(script)
104
+ script = script.first if script.is_a?(Array)
105
+
106
+ buf = FFI::MemoryPointer.new(:char, script.bytesize)
107
+ buf.put_bytes(0, script)
108
+
109
+ result = Bindings.store_script(buf, script.bytesize)
110
+
111
+ hash_buffer = Bindings::ScriptHashBuffer.new(result)
112
+ hash_buffer[:ptr].read_string(hash_buffer[:len])
113
+ end
114
+
115
+ # Execute a Lua script on the server.
116
+ #
117
+ # @param [String] script the Lua script to execute
118
+ # @param [Array<String>] keys array of key names that the script will access
119
+ # @param [Array<Object>] args array of arguments to pass to the script
120
+ # @return [Object] the result of the script execution
121
+ # @raise [ArgumentError] if script is empty
122
+ # @raise [CommandError] if script execution fails
123
+ #
124
+ # @example Execute a simple script
125
+ # valkey.eval("return 1")
126
+ # # => 1
127
+ # @example Execute script with keys and arguments
128
+ # valkey.eval("return KEYS[1] .. ARGV[1]", keys: ["mykey"], args: ["myarg"])
129
+ # # => "mykeynyarg"
130
+ # @example Execute script with multiple keys and arguments
131
+ # valkey.eval("return #KEYS + #ARGV", keys: ["key1", "key2"], args: ["arg1", "arg2", "arg3"])
132
+ # # => 5
133
+ # @example Execute script that returns different data types
134
+ # valkey.eval("return {1, 'hello', true, nil}")
135
+ # # => [1, "hello", true, nil]
136
+ # Since the eval is not available in the rust backend
137
+ # using the load and invoke script
138
+ def eval(script, keys: [], args: [])
139
+ # Validate script parameter
140
+ raise ArgumentError, "script must be a string" unless script.is_a?(String)
141
+ raise ArgumentError, "script cannot be empty" if script.empty?
142
+
143
+ # Validate and convert keys and args to strings
144
+ begin
145
+ keys = Array(keys).map(&:to_s)
146
+ args = Array(args).map(&:to_s)
147
+ rescue StandardError => e
148
+ raise ArgumentError, "failed to convert keys or args to strings: #{e.message}"
149
+ end
150
+
151
+ # Load script to get SHA1 hash, then execute via invoke_script
152
+ sha = script_load(script)
153
+ invoke_script(sha, keys: keys, args: args)
154
+ end
155
+
156
+ # Execute a cached Lua script by its SHA1 hash.
157
+ #
158
+ # @param [String] sha the SHA1 hash of the script to execute
159
+ # @param [Array<String>] keys array of key names that the script will access
160
+ # @param [Array<Object>] args array of arguments to pass to the script
161
+ # @return [Object] the result of the script execution
162
+ # @raise [ArgumentError] if SHA1 hash format is invalid
163
+ # @raise [CommandError] if script is not found or execution fails
164
+ #
165
+ # @example Execute a cached script
166
+ # sha = valkey.script_load("return 1")
167
+ # valkey.evalsha(sha)
168
+ # # => 1
169
+ # @example Execute cached script with parameters
170
+ # script = "return KEYS[1] .. ':' .. ARGV[1]"
171
+ # sha = valkey.script_load(script)
172
+ # valkey.evalsha(sha, keys: ["user"], args: ["123"])
173
+ # # => "user:123"
174
+ # @example Handle script not found error
175
+ # begin
176
+ # valkey.evalsha("nonexistent_sha", keys: [], args: [])
177
+ # rescue Valkey::CommandError => e
178
+ # puts "Script not found: #{e.message}"
179
+ # end
180
+ # Since evalsha is not available in rust backend
181
+ # using invoke script
182
+ def evalsha(sha, keys: [], args: [])
183
+ # Validate SHA1 hash parameter
184
+ raise ArgumentError, "sha1 hash must be a string" unless sha.is_a?(String)
185
+ raise ArgumentError, "sha1 hash must be a 40-character hexadecimal string" unless valid_sha1?(sha)
186
+
187
+ # Validate and convert keys and args to strings
188
+ begin
189
+ keys = Array(keys).map(&:to_s)
190
+ args = Array(args).map(&:to_s)
191
+ rescue StandardError => e
192
+ raise ArgumentError, "failed to convert keys or args to strings: #{e.message}"
193
+ end
194
+
195
+ # Execute cached script via invoke_script
196
+ invoke_script(sha, keys: keys, args: args)
197
+ end
198
+
199
+ # Execute a read-only Lua script on the server.
200
+ #
201
+ # This is a read-only variant of EVAL that cannot execute commands
202
+ # that modify data. It can be routed to read replicas.
203
+ #
204
+ # @param [String] script the Lua script to execute
205
+ # @param [Array<String>] keys array of key names that the script will access
206
+ # @param [Array<Object>] args array of arguments to pass to the script
207
+ # @return [Object] the result of the script execution
208
+ #
209
+ # @example Execute a read-only script
210
+ # valkey.eval_ro("return redis.call('get', KEYS[1])", keys: ["mykey"])
211
+ # # => "myvalue"
212
+ #
213
+ # @see https://valkey.io/commands/eval_ro/
214
+ def eval_ro(script, keys: [], args: [])
215
+ raise ArgumentError, "script must be a string" unless script.is_a?(String)
216
+ raise ArgumentError, "script cannot be empty" if script.empty?
217
+
218
+ keys = Array(keys).map(&:to_s)
219
+ args = Array(args).map(&:to_s)
220
+
221
+ sha = script_load(script)
222
+ invoke_script(sha, keys: keys, args: args)
223
+ end
224
+
225
+ # Execute a cached read-only Lua script by its SHA1 hash.
226
+ #
227
+ # This is a read-only variant of EVALSHA that cannot execute commands
228
+ # that modify data. It can be routed to read replicas.
229
+ #
230
+ # @param [String] sha the SHA1 hash of the script to execute
231
+ # @param [Array<String>] keys array of key names that the script will access
232
+ # @param [Array<Object>] args array of arguments to pass to the script
233
+ # @return [Object] the result of the script execution
234
+ #
235
+ # @example Execute a cached read-only script
236
+ # sha = valkey.script_load("return redis.call('get', KEYS[1])")
237
+ # valkey.evalsha_ro(sha, keys: ["mykey"])
238
+ # # => "myvalue"
239
+ #
240
+ # @see https://valkey.io/commands/evalsha_ro/
241
+ def evalsha_ro(sha, keys: [], args: [])
242
+ raise ArgumentError, "sha1 hash must be a string" unless sha.is_a?(String)
243
+ raise ArgumentError, "sha1 hash must be a 40-character hexadecimal string" unless valid_sha1?(sha)
244
+
245
+ keys = Array(keys).map(&:to_s)
246
+ args = Array(args).map(&:to_s)
247
+
248
+ invoke_script(sha, keys: keys, args: args)
249
+ end
250
+
251
+ def invoke_script(script, args: [], keys: [])
252
+ arg_ptrs, arg_lens = build_command_args(args)
253
+ keys_ptrs, keys_lens = build_command_args(keys)
254
+
255
+ route = ""
256
+ route_buf = FFI::MemoryPointer.from_string(route)
257
+
258
+ sha = FFI::MemoryPointer.new(:char, script.bytesize + 1)
259
+ sha.put_bytes(0, script)
260
+
261
+ res = Bindings.invoke_script(
262
+ @connection,
263
+ 0,
264
+ sha,
265
+ keys.size,
266
+ keys_ptrs,
267
+ keys_lens,
268
+ args.size,
269
+ arg_ptrs,
270
+ arg_lens,
271
+ route_buf,
272
+ route.bytesize
273
+ )
274
+
275
+ convert_response(res)
276
+ end
277
+
278
+ private
279
+
280
+ # Validate SHA1 hash format (40-character hexadecimal string)
281
+ def valid_sha1?(sha)
282
+ sha.is_a?(String) && sha.length == 40 && sha.match?(/\A[a-fA-F0-9]{40}\z/)
283
+ end
284
+ end
285
+ end
286
+ end