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,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ module Commands
5
+ # This module contains commands related to RediSearch Vector Search.
6
+ #
7
+ # RediSearch provides secondary indexing, full-text search, and vector similarity search
8
+ # capabilities on top of Redis/Valkey. These commands require the RediSearch module to be loaded.
9
+ #
10
+ # @see https://redis.io/docs/stack/search/
11
+ #
12
+ module VectorSearchCommands
13
+ # List all available indexes.
14
+ #
15
+ # @example List all indexes
16
+ # valkey.ft_list
17
+ # # => ["idx1", "idx2"]
18
+ #
19
+ # @return [Array<String>] array of index names
20
+ #
21
+ # @see https://redis.io/commands/ft._list/
22
+ def ft_list
23
+ send_command(RequestType::FT_LIST)
24
+ end
25
+
26
+ # Run a search query with aggregations.
27
+ #
28
+ # @example Perform an aggregation query
29
+ # valkey.ft_aggregate("myIndex", "*", "GROUPBY", "1", "@category", "REDUCE", "COUNT", "0", "AS", "count")
30
+ # # => [[1, ["category", "electronics", "count", "5"]]]
31
+ #
32
+ # @param [String] index the index name to search
33
+ # @param [String] query the search query
34
+ # @param [Array<String>] args additional query arguments (GROUPBY, REDUCE, etc.)
35
+ # @return [Array] aggregation results
36
+ #
37
+ # @see https://redis.io/commands/ft.aggregate/
38
+ def ft_aggregate(index, query, *args)
39
+ command_args = [index, query] + args
40
+ send_command(RequestType::FT_AGGREGATE, command_args)
41
+ end
42
+
43
+ # Add an alias to an index.
44
+ #
45
+ # @example Add an alias to an index
46
+ # valkey.ft_alias_add("myAlias", "myIndex")
47
+ # # => "OK"
48
+ #
49
+ # @param [String] alias the alias name
50
+ # @param [String] index the index name
51
+ # @return [String] "OK" on success
52
+ #
53
+ # @see https://redis.io/commands/ft.aliasadd/
54
+ def ft_alias_add(alias_name, index)
55
+ send_command(RequestType::FT_ALIAS_ADD, [alias_name, index])
56
+ end
57
+
58
+ # Delete an alias from an index.
59
+ #
60
+ # @example Delete an alias
61
+ # valkey.ft_alias_del("myAlias")
62
+ # # => "OK"
63
+ #
64
+ # @param [String] alias the alias name to delete
65
+ # @return [String] "OK" on success
66
+ #
67
+ # @see https://redis.io/commands/ft.aliasdel/
68
+ def ft_alias_del(alias_name)
69
+ send_command(RequestType::FT_ALIAS_DEL, [alias_name])
70
+ end
71
+
72
+ # List all existing aliases.
73
+ #
74
+ # @example List all aliases
75
+ # valkey.ft_alias_list
76
+ # # => ["alias1", "alias2"]
77
+ #
78
+ # @return [Array<String>] array of alias names
79
+ #
80
+ # @see https://redis.io/commands/ft.aliaslist/
81
+ def ft_alias_list
82
+ send_command(RequestType::FT_ALIAS_LIST)
83
+ end
84
+
85
+ # Update an alias to point to a different index.
86
+ #
87
+ # @example Update an alias
88
+ # valkey.ft_alias_update("myAlias", "newIndex")
89
+ # # => "OK"
90
+ #
91
+ # @param [String] alias the alias name
92
+ # @param [String] index the new index name
93
+ # @return [String] "OK" on success
94
+ #
95
+ # @see https://redis.io/commands/ft.aliasupdate/
96
+ def ft_alias_update(alias_name, index)
97
+ send_command(RequestType::FT_ALIAS_UPDATE, [alias_name, index])
98
+ end
99
+
100
+ # Create a search index with the given schema.
101
+ #
102
+ # @example Create a basic index
103
+ # valkey.ft_create("myIndex", "SCHEMA", "title", "TEXT", "price", "NUMERIC")
104
+ # # => "OK"
105
+ #
106
+ # @example Create an index with vector field
107
+ # valkey.ft_create("vecIndex", "ON", "HASH", "PREFIX", "1", "doc:",
108
+ # "SCHEMA", "embedding", "VECTOR", "HNSW", "6",
109
+ # "TYPE", "FLOAT32", "DIM", "128", "DISTANCE_METRIC", "COSINE")
110
+ # # => "OK"
111
+ #
112
+ # @param [String] index the index name
113
+ # @param [Array<String>] args schema definition and options
114
+ # @return [String] "OK" on success
115
+ #
116
+ # @see https://redis.io/commands/ft.create/
117
+ def ft_create(index, *args)
118
+ command_args = [index] + args
119
+ send_command(RequestType::FT_CREATE, command_args)
120
+ end
121
+
122
+ # Drop an index and optionally delete all documents.
123
+ #
124
+ # @example Drop an index without deleting documents
125
+ # valkey.ft_drop_index("myIndex")
126
+ # # => "OK"
127
+ #
128
+ # @example Drop an index and delete all documents
129
+ # valkey.ft_drop_index("myIndex", dd: true)
130
+ # # => "OK"
131
+ #
132
+ # @param [String] index the index name
133
+ # @param [Boolean] dd whether to delete documents (DD flag)
134
+ # @return [String] "OK" on success
135
+ #
136
+ # @see https://redis.io/commands/ft.dropindex/
137
+ def ft_drop_index(index, dd: false)
138
+ args = [index]
139
+ args << "DD" if dd
140
+ send_command(RequestType::FT_DROP_INDEX, args)
141
+ end
142
+
143
+ # Explain how a query is parsed and executed.
144
+ #
145
+ # @example Explain a query
146
+ # valkey.ft_explain("myIndex", "@title:hello @price:[0 100]")
147
+ # # => "INTERSECT {\n @title:hello\n @price:[0 100]\n}\n"
148
+ #
149
+ # @param [String] index the index name
150
+ # @param [String] query the search query
151
+ # @param [Array<String>] args additional query arguments
152
+ # @return [String] query execution plan
153
+ #
154
+ # @see https://redis.io/commands/ft.explain/
155
+ def ft_explain(index, query, *args)
156
+ command_args = [index, query] + args
157
+ send_command(RequestType::FT_EXPLAIN, command_args)
158
+ end
159
+
160
+ # Explain how a query is parsed and executed (CLI-formatted output).
161
+ #
162
+ # @example Explain a query in CLI format
163
+ # valkey.ft_explain_cli("myIndex", "@title:hello")
164
+ # # => formatted query plan
165
+ #
166
+ # @param [String] index the index name
167
+ # @param [String] query the search query
168
+ # @param [Array<String>] args additional query arguments
169
+ # @return [String] formatted query execution plan
170
+ #
171
+ # @see https://redis.io/commands/ft.explaincli/
172
+ def ft_explain_cli(index, query, *args)
173
+ command_args = [index, query] + args
174
+ send_command(RequestType::FT_EXPLAIN_CLI, command_args)
175
+ end
176
+
177
+ # Get information about an index.
178
+ #
179
+ # @example Get index info
180
+ # valkey.ft_info("myIndex")
181
+ # # => ["index_name", "myIndex", "fields", [...], ...]
182
+ #
183
+ # @param [String] index the index name
184
+ # @return [Array] index information as array of key-value pairs
185
+ #
186
+ # @see https://redis.io/commands/ft.info/
187
+ def ft_info(index)
188
+ send_command(RequestType::FT_INFO, [index])
189
+ end
190
+
191
+ # Profile a search or aggregation query.
192
+ #
193
+ # @example Profile a search query
194
+ # valkey.ft_profile("myIndex", "SEARCH", "QUERY", "@title:hello")
195
+ # # => [execution time, results]
196
+ #
197
+ # @example Profile an aggregation query
198
+ # valkey.ft_profile("myIndex", "AGGREGATE", "QUERY", "*", "GROUPBY", "1", "@category")
199
+ # # => [execution time, results]
200
+ #
201
+ # @param [String] index the index name
202
+ # @param [String] query_type either "SEARCH" or "AGGREGATE"
203
+ # @param [Array<String>] args query arguments
204
+ # @return [Array] profiling results with execution time and query results
205
+ #
206
+ # @see https://redis.io/commands/ft.profile/
207
+ def ft_profile(index, query_type, *args)
208
+ command_args = [index, query_type] + args
209
+ send_command(RequestType::FT_PROFILE, command_args)
210
+ end
211
+
212
+ # Search an index with a query.
213
+ #
214
+ # @example Basic search
215
+ # valkey.ft_search("myIndex", "hello world")
216
+ # # => [1, "doc1", ["title", "hello world"]]
217
+ #
218
+ # @example Search with options
219
+ # valkey.ft_search("myIndex", "@title:hello", "LIMIT", "0", "10", "RETURN", "2", "title", "price")
220
+ # # => [total_results, doc_id, [field1, value1, field2, value2], ...]
221
+ #
222
+ # @example Vector similarity search
223
+ # valkey.ft_search("vecIndex", "*=>[KNN 5 @embedding $vec]",
224
+ # "PARAMS", "2", "vec", vector_blob,
225
+ # "RETURN", "1", "__embedding_score",
226
+ # "DIALECT", "2")
227
+ # # => [results_count, doc_id, ["__embedding_score", "0.95"], ...]
228
+ #
229
+ # @param [String] index the index name
230
+ # @param [String] query the search query
231
+ # @param [Array<String>] args additional query arguments (LIMIT, RETURN, SORTBY, etc.)
232
+ # @return [Array] search results with total count and matching documents
233
+ #
234
+ # @see https://redis.io/commands/ft.search/
235
+ def ft_search(index, query, *args)
236
+ command_args = [index, query] + args
237
+ send_command(RequestType::FT_SEARCH, command_args)
238
+ end
239
+
240
+ # Convenience method for FT.* commands.
241
+ #
242
+ # @example List indexes
243
+ # valkey.ft(:list)
244
+ # # => ["idx1", "idx2"]
245
+ #
246
+ # @example Create an index
247
+ # valkey.ft(:create, "myIndex", "SCHEMA", "title", "TEXT")
248
+ # # => "OK"
249
+ #
250
+ # @example Search an index
251
+ # valkey.ft(:search, "myIndex", "hello")
252
+ # # => [results]
253
+ #
254
+ # @param [String, Symbol] subcommand the subcommand (list, create, search, etc.)
255
+ # @param [Array] args arguments for the subcommand
256
+ # @param [Hash] options options for the subcommand
257
+ # @return [Object] depends on subcommand
258
+ def ft(subcommand, *args, **options)
259
+ subcommand = subcommand.to_s.downcase.gsub("-", "_")
260
+
261
+ if args.empty? && options.empty?
262
+ send("ft_#{subcommand}")
263
+ elsif options.empty?
264
+ send("ft_#{subcommand}", *args)
265
+ else
266
+ send("ft_#{subcommand}", *args, **options)
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "valkey/commands/string_commands"
4
+ require "valkey/commands/connection_commands"
5
+ require "valkey/commands/server_commands"
6
+ require "valkey/commands/generic_commands"
7
+ require "valkey/commands/bitmap_commands"
8
+ require "valkey/commands/list_commands"
9
+ require "valkey/commands/geo_commands"
10
+ require "valkey/commands/hyper_log_log_commands"
11
+ require "valkey/commands/sorted_set_commands"
12
+ require "valkey/commands/set_commands"
13
+ require "valkey/commands/scripting_commands"
14
+ require "valkey/commands/function_commands"
15
+ require "valkey/commands/module_commands"
16
+ require "valkey/commands/pubsub_commands"
17
+ require "valkey/commands/json_commands"
18
+ require "valkey/commands/cluster_commands"
19
+ require "valkey/commands/transaction_commands"
20
+ require "valkey/commands/vector_search_commands"
21
+ require "valkey/commands/stream_commands"
22
+ require "valkey/commands/hash_commands"
23
+
24
+ class Valkey
25
+ # Valkey commands module
26
+ #
27
+ # This module includes various command modules that provide methods
28
+ # for interacting with a Valkey server. Each command module corresponds to a
29
+ # specific set of commands that can be executed against the Valkey server.
30
+ #
31
+ # The commands are organized into groups based on their functionality,
32
+ # such as string operations, connection management, server information,
33
+ # key management, and bitmap operations.
34
+ #
35
+ # @see https://valkey.io/commands/ Valkey Commands Documentation
36
+ #
37
+ module Commands
38
+ include StringCommands
39
+ include ConnectionCommands
40
+ include ServerCommands
41
+ include GenericCommands
42
+ include BitmapCommands
43
+ include ListCommands
44
+ include GeoCommands
45
+ include HyperLogLogCommands
46
+ include SortedSetCommands
47
+ include SetCommands
48
+ include ScriptingCommands
49
+ include FunctionCommands
50
+ include ModuleCommands
51
+ include PubSubCommands
52
+ include JsonCommands
53
+ include ClusterCommands
54
+ include TransactionCommands
55
+ include VectorSearchCommands
56
+ include StreamCommands
57
+ include HashCommands
58
+
59
+ # Commands that are not implemented by design.
60
+ # Raises CommandError when called.
61
+ #
62
+ %i[].each do |command|
63
+ define_method command do |*_args|
64
+ raise CommandError, "Unsupported command: #{command}"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ class BaseError < StandardError; end
5
+
6
+ class ProtocolError < BaseError
7
+ def initialize(reply_type)
8
+ super(<<-MESSAGE.gsub(/(?:^|\n)\s*/, " "))
9
+ Got '#{reply_type}' as initial reply byte.
10
+ If you're in a forking environment, such as Unicorn, you need to
11
+ connect to Valkey after forking.
12
+ MESSAGE
13
+ end
14
+ end
15
+
16
+ class CommandError < BaseError; end
17
+
18
+ class PermissionError < CommandError; end
19
+
20
+ class WrongTypeError < CommandError; end
21
+
22
+ class OutOfMemoryError < CommandError; end
23
+
24
+ class NoScriptError < CommandError; end
25
+
26
+ class BaseConnectionError < BaseError; end
27
+
28
+ class CannotConnectError < BaseConnectionError; end
29
+
30
+ class ConnectionError < BaseConnectionError; end
31
+
32
+ class TimeoutError < BaseConnectionError; end
33
+
34
+ class InheritedError < BaseConnectionError; end
35
+
36
+ class ReadOnlyError < BaseConnectionError; end
37
+
38
+ class InvalidClientOptionError < BaseError; end
39
+
40
+ class SubscriptionError < BaseError; end
41
+ end
Binary file
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ # OpenTelemetry integration for Valkey GLIDE Ruby client.
5
+ #
6
+ # This module provides integration with the OpenTelemetry implementation
7
+ # built into the Valkey GLIDE core (Rust FFI layer). Unlike typical Ruby
8
+ # OpenTelemetry instrumentation, this directly configures the native
9
+ # OpenTelemetry exporter in the Rust layer.
10
+ #
11
+ # @example Initialize with HTTP collector
12
+ # Valkey::OpenTelemetry.init(
13
+ # traces: {
14
+ # endpoint: "http://localhost:4318/v1/traces",
15
+ # sample_percentage: 10
16
+ # },
17
+ # metrics: {
18
+ # endpoint: "http://localhost:4318/v1/metrics"
19
+ # },
20
+ # flush_interval_ms: 5000
21
+ # )
22
+ #
23
+ # @example Initialize with gRPC collector
24
+ # Valkey::OpenTelemetry.init(
25
+ # traces: {
26
+ # endpoint: "grpc://localhost:4317",
27
+ # sample_percentage: 1
28
+ # },
29
+ # metrics: {
30
+ # endpoint: "grpc://localhost:4317"
31
+ # }
32
+ # )
33
+ #
34
+ # @example Initialize with file exporter (for testing)
35
+ # Valkey::OpenTelemetry.init(
36
+ # traces: {
37
+ # endpoint: "file:///tmp/valkey_traces.json",
38
+ # sample_percentage: 100
39
+ # },
40
+ # metrics: {
41
+ # endpoint: "file:///tmp/valkey_metrics.json"
42
+ # }
43
+ # )
44
+ module OpenTelemetry
45
+ class << self
46
+ @initialized = false
47
+ @config = nil
48
+
49
+ # Initialize OpenTelemetry in the Valkey GLIDE core.
50
+ #
51
+ # This method can only be called once per process. Subsequent calls will
52
+ # be ignored with a warning.
53
+ #
54
+ # @param traces [Hash, nil] Traces configuration
55
+ # @option traces [String] :endpoint The endpoint URL (required)
56
+ # Supported formats:
57
+ # - HTTP: http://localhost:4318/v1/traces
58
+ # - gRPC: grpc://localhost:4317
59
+ # - File: file:///absolute/path/to/traces.json
60
+ # @option traces [Integer] :sample_percentage Sample percentage 0-100 (default: 1)
61
+ # Keep low (1-5%) in production for performance
62
+ #
63
+ # @param metrics [Hash, nil] Metrics configuration
64
+ # @option metrics [String] :endpoint The endpoint URL (required)
65
+ # Same format as traces endpoint
66
+ #
67
+ # @param flush_interval_ms [Integer, nil] Flush interval in milliseconds (default: 5000)
68
+ # Must be a positive integer
69
+ #
70
+ # @raise [ArgumentError] if neither traces nor metrics is provided
71
+ # @raise [ArgumentError] if sample_percentage is not between 0-100
72
+ # @raise [RuntimeError] if initialization fails
73
+ #
74
+ # @return [void]
75
+ def init(traces: nil, metrics: nil, flush_interval_ms: nil)
76
+ if @initialized
77
+ warn "Valkey::OpenTelemetry already initialized - ignoring new configuration"
78
+ return
79
+ end
80
+
81
+ # Validate input
82
+ raise ArgumentError, "At least one of traces or metrics must be provided" if traces.nil? && metrics.nil?
83
+
84
+ if traces && traces[:sample_percentage]
85
+ sample = traces[:sample_percentage]
86
+ unless sample.is_a?(Integer) && sample >= 0 && sample <= 100
87
+ raise ArgumentError, "sample_percentage must be an integer between 0 and 100, got: #{sample}"
88
+ end
89
+ end
90
+
91
+ if flush_interval_ms && (!flush_interval_ms.is_a?(Integer) || flush_interval_ms <= 0)
92
+ raise ArgumentError, "flush_interval_ms must be a positive integer, got: #{flush_interval_ms}"
93
+ end
94
+
95
+ # Build the configuration
96
+ config = build_config(traces, metrics, flush_interval_ms)
97
+
98
+ # Call the FFI function
99
+ error_ptr = Bindings.init_open_telemetry(config)
100
+
101
+ unless error_ptr.null?
102
+ error_msg = error_ptr.read_string
103
+ Bindings.free_c_string(error_ptr)
104
+ raise "Failed to initialize OpenTelemetry: #{error_msg}"
105
+ end
106
+
107
+ @initialized = true
108
+ @config = { traces: traces, metrics: metrics, flush_interval_ms: flush_interval_ms }
109
+
110
+ puts "✅ Valkey OpenTelemetry initialized successfully"
111
+ puts " Traces: #{traces ? traces[:endpoint] : 'disabled'}"
112
+ puts " Metrics: #{metrics ? metrics[:endpoint] : 'disabled'}"
113
+ end
114
+
115
+ # Check if OpenTelemetry has been initialized.
116
+ #
117
+ # @return [Boolean] true if initialized
118
+ def initialized?
119
+ @initialized
120
+ end
121
+
122
+ # Determine if the current request should be sampled based on the configured sample percentage.
123
+ #
124
+ # @return [Boolean] true if the request should be sampled
125
+ def should_sample?
126
+ return false unless @initialized
127
+ return false unless @config&.dig(:traces)
128
+
129
+ sample_percentage = @config.dig(:traces, :sample_percentage) || 1
130
+ rand(100) < sample_percentage
131
+ end
132
+
133
+ # Get the current OpenTelemetry configuration.
134
+ #
135
+ # @return [Hash, nil] the configuration hash or nil if not initialized
136
+ attr_reader :config
137
+
138
+ # Reset initialization state (for testing only).
139
+ #
140
+ # @api private
141
+ def reset!
142
+ @initialized = false
143
+ @config = nil
144
+ end
145
+
146
+ private
147
+
148
+ def build_config(traces, metrics, flush_interval_ms)
149
+ config_struct = Bindings::OpenTelemetryConfig.new
150
+
151
+ # Configure traces if provided
152
+ if traces
153
+ validate_endpoint!(traces[:endpoint], "traces")
154
+
155
+ traces_struct = Bindings::OpenTelemetryTracesConfig.new
156
+ traces_struct[:endpoint] = FFI::MemoryPointer.from_string(traces[:endpoint])
157
+
158
+ if traces[:sample_percentage]
159
+ traces_struct[:has_sample_percentage] = true
160
+ traces_struct[:sample_percentage] = traces[:sample_percentage]
161
+ else
162
+ traces_struct[:has_sample_percentage] = false
163
+ traces_struct[:sample_percentage] = 1 # Default
164
+ end
165
+
166
+ config_struct[:traces] = traces_struct.pointer
167
+ else
168
+ config_struct[:traces] = FFI::Pointer::NULL
169
+ end
170
+
171
+ # Configure metrics if provided
172
+ if metrics
173
+ validate_endpoint!(metrics[:endpoint], "metrics")
174
+
175
+ metrics_struct = Bindings::OpenTelemetryMetricsConfig.new
176
+ metrics_struct[:endpoint] = FFI::MemoryPointer.from_string(metrics[:endpoint])
177
+ config_struct[:metrics] = metrics_struct.pointer
178
+ else
179
+ config_struct[:metrics] = FFI::Pointer::NULL
180
+ end
181
+
182
+ # Configure flush interval
183
+ if flush_interval_ms
184
+ config_struct[:has_flush_interval_ms] = true
185
+ config_struct[:flush_interval_ms] = flush_interval_ms
186
+ else
187
+ config_struct[:has_flush_interval_ms] = false
188
+ config_struct[:flush_interval_ms] = 5000 # Default
189
+ end
190
+
191
+ config_struct
192
+ end
193
+
194
+ def validate_endpoint!(endpoint, type)
195
+ unless endpoint.is_a?(String) && !endpoint.empty?
196
+ raise ArgumentError, "#{type} endpoint must be a non-empty string"
197
+ end
198
+
199
+ # Validate endpoint format
200
+ valid_prefixes = %w[http:// https:// grpc:// file://]
201
+ return if valid_prefixes.any? { |prefix| endpoint.start_with?(prefix) }
202
+
203
+ raise ArgumentError, "#{type} endpoint must start with one of: #{valid_prefixes.join(', ')}"
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ class Pipeline
5
+ include Commands
6
+
7
+ attr_reader :commands
8
+
9
+ def initialize
10
+ @commands = []
11
+ # Keep transactional state consistent with the main client so that
12
+ # helpers like `multi`/`exec` can safely consult `@in_multi`.
13
+ @in_multi = false
14
+ end
15
+
16
+ def send_command(command_type, command_args = [], &block)
17
+ @commands << [command_type, command_args, block]
18
+ end
19
+ end
20
+ end