valkey-rb 0.3.5

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 +43 -0
  3. data/.rubocop_todo.yml +22 -0
  4. data/README.md +34 -0
  5. data/Rakefile +23 -0
  6. data/lib/valkey/bindings.rb +173 -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 +454 -0
  12. data/lib/valkey/commands/geo_commands.rb +87 -0
  13. data/lib/valkey/commands/hash_commands.rb +586 -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 +217 -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 +756 -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 +69 -0
  28. data/lib/valkey/errors.rb +41 -0
  29. data/lib/valkey/libglide_ffi.dylib +0 -0
  30. data/lib/valkey/libglide_ffi.so +0 -0
  31. data/lib/valkey/pipeline.rb +20 -0
  32. data/lib/valkey/protobuf/command_request_pb.rb +30 -0
  33. data/lib/valkey/protobuf/connection_request_pb.rb +28 -0
  34. data/lib/valkey/protobuf/response_pb.rb +18 -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 +477 -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,217 @@
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
+ def script_load(script)
87
+ script = script.is_a?(Array) ? script.first : script
88
+
89
+ buf = FFI::MemoryPointer.new(:char, script.bytesize)
90
+ buf.put_bytes(0, script)
91
+
92
+ result = Bindings.store_script(buf, script.bytesize)
93
+
94
+ hash_buffer = Bindings::ScriptHashBuffer.new(result)
95
+ hash_buffer[:ptr].read_string(hash_buffer[:len])
96
+ end
97
+
98
+ # Execute a Lua script on the server.
99
+ #
100
+ # @param [String] script the Lua script to execute
101
+ # @param [Array<String>] keys array of key names that the script will access
102
+ # @param [Array<Object>] args array of arguments to pass to the script
103
+ # @return [Object] the result of the script execution
104
+ # @raise [ArgumentError] if script is empty
105
+ # @raise [CommandError] if script execution fails
106
+ #
107
+ # @example Execute a simple script
108
+ # valkey.eval("return 1")
109
+ # # => 1
110
+ # @example Execute script with keys and arguments
111
+ # valkey.eval("return KEYS[1] .. ARGV[1]", keys: ["mykey"], args: ["myarg"])
112
+ # # => "mykeynyarg"
113
+ # @example Execute script with multiple keys and arguments
114
+ # valkey.eval("return #KEYS + #ARGV", keys: ["key1", "key2"], args: ["arg1", "arg2", "arg3"])
115
+ # # => 5
116
+ # @example Execute script that returns different data types
117
+ # valkey.eval("return {1, 'hello', true, nil}")
118
+ # # => [1, "hello", true, nil]
119
+ # Since the eval is not available in the rust backend
120
+ # using the load and invoke script
121
+ def eval(script, keys: [], args: [])
122
+ # Validate script parameter
123
+ raise ArgumentError, "script must be a string" unless script.is_a?(String)
124
+ raise ArgumentError, "script cannot be empty" if script.empty?
125
+
126
+ # Validate and convert keys and args to strings
127
+ begin
128
+ keys = Array(keys).map(&:to_s)
129
+ args = Array(args).map(&:to_s)
130
+ rescue StandardError => e
131
+ raise ArgumentError, "failed to convert keys or args to strings: #{e.message}"
132
+ end
133
+
134
+ # Load script to get SHA1 hash, then execute via invoke_script
135
+ sha = script_load(script)
136
+ invoke_script(sha, keys: keys, args: args)
137
+ end
138
+
139
+ # Execute a cached Lua script by its SHA1 hash.
140
+ #
141
+ # @param [String] sha the SHA1 hash of the script to execute
142
+ # @param [Array<String>] keys array of key names that the script will access
143
+ # @param [Array<Object>] args array of arguments to pass to the script
144
+ # @return [Object] the result of the script execution
145
+ # @raise [ArgumentError] if SHA1 hash format is invalid
146
+ # @raise [CommandError] if script is not found or execution fails
147
+ #
148
+ # @example Execute a cached script
149
+ # sha = valkey.script_load("return 1")
150
+ # valkey.evalsha(sha)
151
+ # # => 1
152
+ # @example Execute cached script with parameters
153
+ # script = "return KEYS[1] .. ':' .. ARGV[1]"
154
+ # sha = valkey.script_load(script)
155
+ # valkey.evalsha(sha, keys: ["user"], args: ["123"])
156
+ # # => "user:123"
157
+ # @example Handle script not found error
158
+ # begin
159
+ # valkey.evalsha("nonexistent_sha", keys: [], args: [])
160
+ # rescue Valkey::CommandError => e
161
+ # puts "Script not found: #{e.message}"
162
+ # end
163
+ # Since evalsha is not available in rust backend
164
+ # using invoke script
165
+ def evalsha(sha, keys: [], args: [])
166
+ # Validate SHA1 hash parameter
167
+ raise ArgumentError, "sha1 hash must be a string" unless sha.is_a?(String)
168
+ raise ArgumentError, "sha1 hash must be a 40-character hexadecimal string" unless valid_sha1?(sha)
169
+
170
+ # Validate and convert keys and args to strings
171
+ begin
172
+ keys = Array(keys).map(&:to_s)
173
+ args = Array(args).map(&:to_s)
174
+ rescue StandardError => e
175
+ raise ArgumentError, "failed to convert keys or args to strings: #{e.message}"
176
+ end
177
+
178
+ # Execute cached script via invoke_script
179
+ invoke_script(sha, keys: keys, args: args)
180
+ end
181
+
182
+ def invoke_script(script, args: [], keys: [])
183
+ arg_ptrs, arg_lens = build_command_args(args)
184
+ keys_ptrs, keys_lens = build_command_args(keys)
185
+
186
+ route = ""
187
+ route_buf = FFI::MemoryPointer.from_string(route)
188
+
189
+ sha = FFI::MemoryPointer.new(:char, script.bytesize + 1)
190
+ sha.put_bytes(0, script)
191
+
192
+ res = Bindings.invoke_script(
193
+ @connection,
194
+ 0,
195
+ sha,
196
+ keys.size,
197
+ keys_ptrs,
198
+ keys_lens,
199
+ args.size,
200
+ arg_ptrs,
201
+ arg_lens,
202
+ route_buf,
203
+ route.bytesize
204
+ )
205
+
206
+ convert_response(res)
207
+ end
208
+
209
+ private
210
+
211
+ # Validate SHA1 hash format (40-character hexadecimal string)
212
+ def valid_sha1?(sha)
213
+ sha.is_a?(String) && sha.length == 40 && sha.match?(/\A[a-fA-F0-9]{40}\z/)
214
+ end
215
+ end
216
+ end
217
+ end