redis 5.4.1 → 6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -1
- data/README.md +165 -12
- data/lib/redis/client.rb +42 -10
- data/lib/redis/commands/geo.rb +108 -6
- data/lib/redis/commands/hashes.rb +166 -2
- data/lib/redis/commands/keys.rb +5 -1
- data/lib/redis/commands/lists.rb +93 -0
- data/lib/redis/commands/modules/json.rb +530 -0
- data/lib/redis/commands/modules/search/aggregation.rb +418 -0
- data/lib/redis/commands/modules/search/dialect.rb +14 -0
- data/lib/redis/commands/modules/search/field.rb +306 -0
- data/lib/redis/commands/modules/search/hybrid.rb +359 -0
- data/lib/redis/commands/modules/search/index.rb +351 -0
- data/lib/redis/commands/modules/search/index_definition.rb +114 -0
- data/lib/redis/commands/modules/search/miscellaneous.rb +607 -0
- data/lib/redis/commands/modules/search/query.rb +738 -0
- data/lib/redis/commands/modules/search/result.rb +488 -0
- data/lib/redis/commands/modules/search/schema.rb +211 -0
- data/lib/redis/commands/modules/search.rb +19 -0
- data/lib/redis/commands/sets.rb +41 -0
- data/lib/redis/commands/sorted_sets.rb +0 -1
- data/lib/redis/commands/streams.rb +28 -8
- data/lib/redis/commands.rb +30 -10
- data/lib/redis/distributed.rb +271 -1
- data/lib/redis/lib_identity.rb +105 -0
- data/lib/redis/subscribe.rb +1 -1
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +141 -11
- metadata +22 -9
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "redis/commands/modules/search/dialect"
|
|
4
|
+
require "redis/commands/modules/search/index_definition"
|
|
5
|
+
require "redis/commands/modules/search/field"
|
|
6
|
+
require "redis/commands/modules/search/schema"
|
|
7
|
+
require "redis/commands/modules/search/query"
|
|
8
|
+
require "redis/commands/modules/search/index"
|
|
9
|
+
require "redis/commands/modules/search/result"
|
|
10
|
+
require "redis/commands/modules/search/miscellaneous"
|
|
11
|
+
require "redis/commands/modules/search/hybrid"
|
|
12
|
+
require "redis/commands/modules/search/aggregation"
|
|
13
|
+
|
|
14
|
+
class Redis
|
|
15
|
+
module Commands
|
|
16
|
+
module Search
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/redis/commands/sets.rb
CHANGED
|
@@ -135,6 +135,26 @@ class Redis
|
|
|
135
135
|
send_command([:sdiffstore, destination].concat(keys))
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
# Get the number of members in the difference between the first set and
|
|
139
|
+
# all successive sets.
|
|
140
|
+
#
|
|
141
|
+
# @example
|
|
142
|
+
# redis.sadd("foo", ["s1", "s2", "s3"])
|
|
143
|
+
# redis.sadd("bar", "s3")
|
|
144
|
+
# redis.sdiffcard("foo", "bar")
|
|
145
|
+
# # => 2
|
|
146
|
+
#
|
|
147
|
+
# @param [String, Array<String>] keys keys pointing to sets to subtract
|
|
148
|
+
# @param [Integer] limit stop counting once the difference reaches `limit`
|
|
149
|
+
# members (`0` means unlimited)
|
|
150
|
+
# @return [Integer] number of members in the difference
|
|
151
|
+
def sdiffcard(*keys, limit: nil)
|
|
152
|
+
keys.flatten!(1)
|
|
153
|
+
args = [:sdiffcard, keys.size].concat(keys)
|
|
154
|
+
args << "LIMIT" << Integer(limit) if limit
|
|
155
|
+
send_command(args)
|
|
156
|
+
end
|
|
157
|
+
|
|
138
158
|
# Intersect multiple sets.
|
|
139
159
|
#
|
|
140
160
|
# @param [String, Array<String>] keys keys pointing to sets to intersect
|
|
@@ -173,6 +193,27 @@ class Redis
|
|
|
173
193
|
send_command([:sunionstore, destination].concat(keys))
|
|
174
194
|
end
|
|
175
195
|
|
|
196
|
+
# Get the number of distinct members in the union of multiple sets.
|
|
197
|
+
#
|
|
198
|
+
# @example
|
|
199
|
+
# redis.sadd("foo", ["s1", "s2", "s3"])
|
|
200
|
+
# redis.sadd("bar", ["s3", "s4"])
|
|
201
|
+
# redis.sunioncard("foo", "bar")
|
|
202
|
+
# # => 4
|
|
203
|
+
#
|
|
204
|
+
# @param [String, Array<String>] keys keys pointing to sets to unify
|
|
205
|
+
# @param [Boolean] approx return an approximate cardinality computed with HyperLogLog
|
|
206
|
+
# @param [Integer] limit stop counting once the union reaches `limit`
|
|
207
|
+
# members (`0` means unlimited)
|
|
208
|
+
# @return [Integer] number of distinct members in the union
|
|
209
|
+
def sunioncard(*keys, approx: false, limit: nil)
|
|
210
|
+
keys.flatten!(1)
|
|
211
|
+
args = [:sunioncard, keys.size].concat(keys)
|
|
212
|
+
args << "APPROX" if approx
|
|
213
|
+
args << "LIMIT" << Integer(limit) if limit
|
|
214
|
+
send_command(args)
|
|
215
|
+
end
|
|
216
|
+
|
|
176
217
|
# Scan a set
|
|
177
218
|
#
|
|
178
219
|
# @example Retrieve the first batch of keys in a set
|
|
@@ -366,7 +366,6 @@ class Redis
|
|
|
366
366
|
# - when `:with_scores` is specified, an array with `[member, score]` pairs
|
|
367
367
|
def zrange(key, start, stop, byscore: false, by_score: byscore, bylex: false, by_lex: bylex,
|
|
368
368
|
rev: false, limit: nil, withscores: false, with_scores: withscores)
|
|
369
|
-
|
|
370
369
|
if by_score && by_lex
|
|
371
370
|
raise ArgumentError, "only one of :by_score or :by_lex can be specified"
|
|
372
371
|
end
|
|
@@ -44,10 +44,11 @@ class Redis
|
|
|
44
44
|
# @option opts [Integer] :maxlen max length of entries to keep
|
|
45
45
|
# @option opts [Integer] :minid min id of entries to keep
|
|
46
46
|
# @option opts [Boolean] :approximate whether to add `~` modifier of maxlen/minid or not
|
|
47
|
+
# @option opts [Integer] :limit maximum count of entries to be evicted, requires approximate trimming
|
|
47
48
|
# @option opts [Boolean] :nomkstream whether to add NOMKSTREAM, default is not to add
|
|
48
49
|
#
|
|
49
50
|
# @return [String] the entry id
|
|
50
|
-
def xadd(key, entry, approximate: nil, maxlen: nil, minid: nil, nomkstream: nil, id: '*')
|
|
51
|
+
def xadd(key, entry, approximate: nil, maxlen: nil, minid: nil, limit: nil, nomkstream: nil, id: '*')
|
|
51
52
|
args = [:xadd, key]
|
|
52
53
|
args << 'NOMKSTREAM' if nomkstream
|
|
53
54
|
if maxlen
|
|
@@ -61,6 +62,7 @@ class Redis
|
|
|
61
62
|
args << "~" if approximate
|
|
62
63
|
args << minid
|
|
63
64
|
end
|
|
65
|
+
args.concat(['LIMIT', limit]) if limit
|
|
64
66
|
args << id
|
|
65
67
|
args.concat(entry.flatten)
|
|
66
68
|
send_command(args)
|
|
@@ -181,18 +183,28 @@ class Redis
|
|
|
181
183
|
# redis.xread(%w[mystream1 mystream2], %w[0-0 0-0])
|
|
182
184
|
# @example With count option
|
|
183
185
|
# redis.xread('mystream', '0-0', count: 2)
|
|
186
|
+
# @example With max_count option
|
|
187
|
+
# redis.xread(%w[mystream1 mystream2], %w[0-0 0-0], max_count: 10)
|
|
184
188
|
# @example With block option
|
|
185
189
|
# redis.xread('mystream', '$', block: 1000)
|
|
186
190
|
#
|
|
187
|
-
# @param keys
|
|
188
|
-
# @param ids
|
|
189
|
-
# @param count
|
|
190
|
-
# @param
|
|
191
|
+
# @param keys [Array<String>] one or multiple stream keys
|
|
192
|
+
# @param ids [Array<String>] one or multiple entry ids
|
|
193
|
+
# @param count [Integer] the number of entries as limit per stream
|
|
194
|
+
# @param max_count [Integer] the total number of entries as limit across all streams
|
|
195
|
+
# combined, must be greater than or equal to `count` when both are given (Redis 8.10+).
|
|
196
|
+
# Defaults to unlimited.
|
|
197
|
+
# @param max_size [Integer] soft cap on the total reply size in bytes across all
|
|
198
|
+
# streams combined, measured on the serialized server reply including protocol overhead;
|
|
199
|
+
# a single oversized first entry can still be returned (Redis 8.10+). Defaults to unlimited.
|
|
200
|
+
# @param block [Integer] the number of milliseconds as blocking timeout
|
|
191
201
|
#
|
|
192
202
|
# @return [Hash{String => Hash{String => Hash}}] the entries
|
|
193
|
-
def xread(keys, ids, count: nil, block: nil)
|
|
203
|
+
def xread(keys, ids, count: nil, max_count: nil, max_size: nil, block: nil)
|
|
194
204
|
args = [:xread]
|
|
195
205
|
args << 'COUNT' << count if count
|
|
206
|
+
args << 'MAXCOUNT' << Integer(max_count) if max_count
|
|
207
|
+
args << 'MAXSIZE' << Integer(max_size) if max_size
|
|
196
208
|
args << 'BLOCK' << block.to_i if block
|
|
197
209
|
_xread(args, keys, ids, block)
|
|
198
210
|
end
|
|
@@ -243,14 +255,22 @@ class Redis
|
|
|
243
255
|
# @param ids [Array<String>] one or multiple entry ids
|
|
244
256
|
# @param opts [Hash] several options for `XREADGROUP` command
|
|
245
257
|
#
|
|
246
|
-
# @option opts [Integer] :count the number of entries as limit
|
|
258
|
+
# @option opts [Integer] :count the number of entries as limit per stream
|
|
259
|
+
# @option opts [Integer] :max_count the total number of entries as limit across all streams
|
|
260
|
+
# combined, must be greater than or equal to `count` when both are given (Redis 8.10+).
|
|
261
|
+
# Defaults to unlimited.
|
|
262
|
+
# @option opts [Integer] :max_size soft cap on the total reply size in bytes across all
|
|
263
|
+
# streams combined, measured on the serialized server reply including protocol overhead;
|
|
264
|
+
# a single oversized first entry can still be returned (Redis 8.10+). Defaults to unlimited.
|
|
247
265
|
# @option opts [Integer] :block the number of milliseconds as blocking timeout
|
|
248
266
|
# @option opts [Boolean] :noack whether message loss is acceptable or not
|
|
249
267
|
#
|
|
250
268
|
# @return [Hash{String => Hash{String => Hash}}] the entries
|
|
251
|
-
def xreadgroup(group, consumer, keys, ids, count: nil, block: nil, noack: nil)
|
|
269
|
+
def xreadgroup(group, consumer, keys, ids, count: nil, max_count: nil, max_size: nil, block: nil, noack: nil)
|
|
252
270
|
args = [:xreadgroup, 'GROUP', group, consumer]
|
|
253
271
|
args << 'COUNT' << count if count
|
|
272
|
+
args << 'MAXCOUNT' << Integer(max_count) if max_count
|
|
273
|
+
args << 'MAXSIZE' << Integer(max_size) if max_size
|
|
254
274
|
args << 'BLOCK' << block.to_i if block
|
|
255
275
|
args << 'NOACK' if noack
|
|
256
276
|
_xread(args, keys, ids, block)
|
data/lib/redis/commands.rb
CHANGED
|
@@ -8,8 +8,10 @@ require "redis/commands/hashes"
|
|
|
8
8
|
require "redis/commands/hyper_log_log"
|
|
9
9
|
require "redis/commands/keys"
|
|
10
10
|
require "redis/commands/lists"
|
|
11
|
+
require "redis/commands/modules/json"
|
|
11
12
|
require "redis/commands/pubsub"
|
|
12
13
|
require "redis/commands/scripting"
|
|
14
|
+
require "redis/commands/modules/search"
|
|
13
15
|
require "redis/commands/server"
|
|
14
16
|
require "redis/commands/sets"
|
|
15
17
|
require "redis/commands/sorted_sets"
|
|
@@ -27,8 +29,10 @@ class Redis
|
|
|
27
29
|
include HyperLogLog
|
|
28
30
|
include Keys
|
|
29
31
|
include Lists
|
|
32
|
+
include Json
|
|
30
33
|
include Pubsub
|
|
31
34
|
include Scripting
|
|
35
|
+
include Search
|
|
32
36
|
include Server
|
|
33
37
|
include Sets
|
|
34
38
|
include SortedSets
|
|
@@ -55,7 +59,9 @@ class Redis
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
Hashify = lambda { |value|
|
|
58
|
-
if value.
|
|
62
|
+
if value.is_a?(Hash) # RESP3 already returns a map
|
|
63
|
+
value
|
|
64
|
+
elsif value.respond_to?(:each_slice) # RESP2 flat [k, v, k, v, ...]
|
|
59
65
|
value.each_slice(2).to_h
|
|
60
66
|
else
|
|
61
67
|
value
|
|
@@ -63,10 +69,12 @@ class Redis
|
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
Pairify = lambda { |value|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
return value unless value.respond_to?(:each_slice)
|
|
73
|
+
|
|
74
|
+
if value.first.is_a?(Array) # RESP3 already returns [[k, v], ...]
|
|
69
75
|
value
|
|
76
|
+
else # RESP2 flat [k, v, k, v, ...]
|
|
77
|
+
value.each_slice(2).to_a
|
|
70
78
|
end
|
|
71
79
|
}
|
|
72
80
|
|
|
@@ -90,7 +98,14 @@ class Redis
|
|
|
90
98
|
FloatifyPairs = lambda { |value|
|
|
91
99
|
return value unless value.respond_to?(:each_slice)
|
|
92
100
|
|
|
93
|
-
value.
|
|
101
|
+
if value.first.is_a?(Array) # RESP3 already returns [[member, score], ...]
|
|
102
|
+
# Scores arrive as native doubles, so the pairs are already in the final shape and
|
|
103
|
+
# re-mapping would only re-allocate identical arrays. Floatify only transforms Strings, so
|
|
104
|
+
# unless a score came back as one (it shouldn't under RESP3) return the parser's array as-is.
|
|
105
|
+
value.first.last.is_a?(String) ? value.map(&FloatifyPair) : value
|
|
106
|
+
else # RESP2 flat [member, score, member, score, ...]
|
|
107
|
+
value.each_slice(2).map(&FloatifyPair)
|
|
108
|
+
end
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
HashifyInfo = lambda { |reply|
|
|
@@ -217,14 +232,19 @@ class Redis
|
|
|
217
232
|
when "get-master-addr-by-name"
|
|
218
233
|
reply
|
|
219
234
|
else
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
235
|
+
case reply
|
|
236
|
+
when Array
|
|
237
|
+
if reply.empty?
|
|
238
|
+
reply # empty list (e.g. sentinels/slaves with no entries) stays []; don't Hashify to {}
|
|
223
239
|
else
|
|
224
|
-
|
|
240
|
+
case reply[0]
|
|
241
|
+
when Array then reply.map(&Hashify) # RESP2: list of flat [k, v, ...] arrays
|
|
242
|
+
when Hash then reply # RESP3: list of maps (already hashes)
|
|
243
|
+
else Hashify.call(reply) # RESP2: a single flat [k, v, ...] array
|
|
244
|
+
end
|
|
225
245
|
end
|
|
226
246
|
else
|
|
227
|
-
reply
|
|
247
|
+
reply # RESP3 single map, or a scalar reply
|
|
228
248
|
end
|
|
229
249
|
end
|
|
230
250
|
end
|
data/lib/redis/distributed.rb
CHANGED
|
@@ -22,6 +22,10 @@ class Redis
|
|
|
22
22
|
@ring = options[:ring] || HashRing.new
|
|
23
23
|
@node_configs = node_configs.map(&:dup)
|
|
24
24
|
@default_options = options.dup
|
|
25
|
+
# Schemas registered via himport_prepare, replayed to nodes that join the ring later (see
|
|
26
|
+
# #add_node) so ring membership changes don't leave nodes without the fieldsets that
|
|
27
|
+
# key-routed himport_set calls expect. Initialized before the add_node loop below.
|
|
28
|
+
@himport_fieldsets = {}
|
|
25
29
|
node_configs.each { |node_config| add_node(node_config) }
|
|
26
30
|
@subscribed_node = nil
|
|
27
31
|
@watch_key = nil
|
|
@@ -43,7 +47,12 @@ class Redis
|
|
|
43
47
|
options = @default_options.merge(options)
|
|
44
48
|
options.delete(:tag)
|
|
45
49
|
options.delete(:ring)
|
|
46
|
-
|
|
50
|
+
node = Redis.new(options)
|
|
51
|
+
# Replay registered fieldsets before the node can receive key-routed himport_set calls:
|
|
52
|
+
# a node joining after a himport_prepare fan-out has neither the server-side fieldset nor
|
|
53
|
+
# a populated registry to self-recover from "no such fieldset".
|
|
54
|
+
@himport_fieldsets.each { |name, fields| node.himport_prepare(name, fields) }
|
|
55
|
+
@ring.add_node node
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
# Change the selected database for the current connection.
|
|
@@ -340,6 +349,124 @@ class Redis
|
|
|
340
349
|
node_for(key).getex(key, **options)
|
|
341
350
|
end
|
|
342
351
|
|
|
352
|
+
# Set the JSON value at a path in the document stored under a key.
|
|
353
|
+
def json_set(key, path, value, **options)
|
|
354
|
+
node_for(key).json_set(key, path, value, **options)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Get the JSON value(s) at one or more paths in the document stored under a key.
|
|
358
|
+
def json_get(key, *paths, **options)
|
|
359
|
+
node_for(key).json_get(key, *paths, **options)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Set one or more JSON values. The keys may live on different nodes and the operation must be
|
|
363
|
+
# atomic, so it cannot be distributed.
|
|
364
|
+
def json_mset(*)
|
|
365
|
+
raise CannotDistribute, :json_mset
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Get the values at a path from several keys. Keys are grouped by node, queried per node, and
|
|
369
|
+
# reassembled in the original key order.
|
|
370
|
+
def json_mget(*keys, path, **options)
|
|
371
|
+
keys.flatten!(1)
|
|
372
|
+
values = keys.group_by { |key| node_for(key) }.each_with_object({}) do |(node, subkeys), acc|
|
|
373
|
+
node.json_mget(*subkeys, path, **options).each_with_index do |value, i|
|
|
374
|
+
acc[subkeys[i]] = value
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
keys.map { |key| values[key] }
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Delete the JSON value(s) at a path in the document stored under a key.
|
|
381
|
+
def json_del(key, path = nil)
|
|
382
|
+
node_for(key).json_del(key, path)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Delete the JSON value(s) at a path in the document stored under a key (alias of json_del).
|
|
386
|
+
def json_forget(key, path = nil)
|
|
387
|
+
node_for(key).json_forget(key, path)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Clear the container and numeric JSON value(s) at a path in the document stored under a key.
|
|
391
|
+
def json_clear(key, path = nil)
|
|
392
|
+
node_for(key).json_clear(key, path)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Merge a JSON value into the document stored under a key at a path.
|
|
396
|
+
def json_merge(key, path, value, **options)
|
|
397
|
+
node_for(key).json_merge(key, path, value, **options)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Append one or more values to the JSON array at a path in the document stored under a key.
|
|
401
|
+
def json_arrappend(key, path, *values, **options)
|
|
402
|
+
node_for(key).json_arrappend(key, path, *values, **options)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Return the index of the first occurrence of a scalar in the JSON array at a path.
|
|
406
|
+
def json_arrindex(key, path, value, **options)
|
|
407
|
+
node_for(key).json_arrindex(key, path, value, **options)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Insert one or more values into the JSON array at a path in the document stored under a key.
|
|
411
|
+
def json_arrinsert(key, path, index, *values, **options)
|
|
412
|
+
node_for(key).json_arrinsert(key, path, index, *values, **options)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Return the length of the JSON array at a path in the document stored under a key.
|
|
416
|
+
def json_arrlen(key, path = nil)
|
|
417
|
+
node_for(key).json_arrlen(key, path)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Remove and return an element from the JSON array at a path in the document stored under a key.
|
|
421
|
+
def json_arrpop(key, path = nil, index = nil, **options)
|
|
422
|
+
node_for(key).json_arrpop(key, path, index, **options)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Trim the JSON array at a path in the document stored under a key to an inclusive range.
|
|
426
|
+
def json_arrtrim(key, path, start, stop)
|
|
427
|
+
node_for(key).json_arrtrim(key, path, start, stop)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Increment the numeric JSON value(s) at a path in the document stored under a key.
|
|
431
|
+
def json_numincrby(key, path, number)
|
|
432
|
+
node_for(key).json_numincrby(key, path, number)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Return the type name(s) of the JSON value(s) at a path in the document stored under a key.
|
|
436
|
+
def json_type(key, path = nil)
|
|
437
|
+
node_for(key).json_type(key, path)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Return the key names of the JSON object(s) at a path in the document stored under a key.
|
|
441
|
+
def json_objkeys(key, path = nil)
|
|
442
|
+
node_for(key).json_objkeys(key, path)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Return the number of keys in the JSON object(s) at a path in the document stored under a key.
|
|
446
|
+
def json_objlen(key, path = nil)
|
|
447
|
+
node_for(key).json_objlen(key, path)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Return the length of the JSON string(s) at a path in the document stored under a key.
|
|
451
|
+
def json_strlen(key, path = nil)
|
|
452
|
+
node_for(key).json_strlen(key, path)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Append a string to the JSON string(s) at a path in the document stored under a key.
|
|
456
|
+
def json_strappend(key, path, value, **options)
|
|
457
|
+
node_for(key).json_strappend(key, path, value, **options)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Toggle the boolean JSON value(s) at a path in the document stored under a key.
|
|
461
|
+
def json_toggle(key, path)
|
|
462
|
+
node_for(key).json_toggle(key, path)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Report the size in bytes of the JSON value(s) at a path in the document stored under a key.
|
|
466
|
+
def json_debug_memory(key, path = nil)
|
|
467
|
+
node_for(key).json_debug_memory(key, path)
|
|
468
|
+
end
|
|
469
|
+
|
|
343
470
|
# Get the values of all the given keys as an Array.
|
|
344
471
|
def mget(*keys)
|
|
345
472
|
keys.flatten!(1)
|
|
@@ -427,6 +554,15 @@ class Redis
|
|
|
427
554
|
end
|
|
428
555
|
end
|
|
429
556
|
|
|
557
|
+
# Remove multiple elements from the head/tail of a list, append/prepend
|
|
558
|
+
# them to another list and return them.
|
|
559
|
+
def lmovem(source, destination, where_source, where_destination, count: nil, exactly: nil, order: nil)
|
|
560
|
+
ensure_same_node(:lmovem, [source, destination]) do |node|
|
|
561
|
+
node.lmovem(source, destination, where_source, where_destination,
|
|
562
|
+
count: count, exactly: exactly, order: order)
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
430
566
|
# Remove the first/last element in a list and append/prepend it
|
|
431
567
|
# to another list and return it, or block until one is available.
|
|
432
568
|
def blmove(source, destination, where_source, where_destination, timeout: 0)
|
|
@@ -435,6 +571,17 @@ class Redis
|
|
|
435
571
|
end
|
|
436
572
|
end
|
|
437
573
|
|
|
574
|
+
# Remove multiple elements from the head/tail of a list, append/prepend
|
|
575
|
+
# them to another list and return them; or block until the request can
|
|
576
|
+
# be satisfied or the timeout expires.
|
|
577
|
+
def blmovem(source, destination, where_source, where_destination, timeout: 0, count: nil, exactly: nil,
|
|
578
|
+
order: nil)
|
|
579
|
+
ensure_same_node(:blmovem, [source, destination]) do |node|
|
|
580
|
+
node.blmovem(source, destination, where_source, where_destination,
|
|
581
|
+
timeout: timeout, count: count, exactly: exactly, order: order)
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
438
585
|
# Prepend one or more values to a list.
|
|
439
586
|
def lpush(key, value)
|
|
440
587
|
node_for(key).lpush(key, value)
|
|
@@ -649,6 +796,15 @@ class Redis
|
|
|
649
796
|
end
|
|
650
797
|
end
|
|
651
798
|
|
|
799
|
+
# Get the number of members in the difference between the first set and
|
|
800
|
+
# all successive sets.
|
|
801
|
+
def sdiffcard(*keys, limit: nil)
|
|
802
|
+
keys.flatten!(1)
|
|
803
|
+
ensure_same_node(:sdiffcard, keys) do |node|
|
|
804
|
+
node.sdiffcard(keys, limit: limit)
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
|
|
652
808
|
# Intersect multiple sets.
|
|
653
809
|
def sinter(*keys)
|
|
654
810
|
keys.flatten!(1)
|
|
@@ -681,6 +837,14 @@ class Redis
|
|
|
681
837
|
end
|
|
682
838
|
end
|
|
683
839
|
|
|
840
|
+
# Get the number of distinct members in the union of multiple sets.
|
|
841
|
+
def sunioncard(*keys, approx: false, limit: nil)
|
|
842
|
+
keys.flatten!(1)
|
|
843
|
+
ensure_same_node(:sunioncard, keys) do |node|
|
|
844
|
+
node.sunioncard(keys, approx: approx, limit: limit)
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
|
|
684
848
|
# Get the number of members in a sorted set.
|
|
685
849
|
def zcard(key)
|
|
686
850
|
node_for(key).zcard(key)
|
|
@@ -918,6 +1082,112 @@ class Redis
|
|
|
918
1082
|
node_for(key).hgetall(key)
|
|
919
1083
|
end
|
|
920
1084
|
|
|
1085
|
+
def hexpire(key, ttl, *fields)
|
|
1086
|
+
node_for(key).hexpire(key, ttl, *fields)
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
def httl(key, *fields)
|
|
1090
|
+
node_for(key).httl(key, *fields)
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
# Scan a hash
|
|
1094
|
+
def hscan(key, cursor, **options)
|
|
1095
|
+
node_for(key).hscan(key, cursor, **options)
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
# Scan a hash and return an enumerator
|
|
1099
|
+
def hscan_each(key, **options, &block)
|
|
1100
|
+
node_for(key).hscan_each(key, **options, &block)
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
# Get the length of the value stored in a hash field
|
|
1104
|
+
def hstrlen(key, field)
|
|
1105
|
+
node_for(key).hstrlen(key, field)
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
def hpexpire(key, ttl, *fields, nx: nil, xx: nil, gt: nil, lt: nil)
|
|
1109
|
+
node_for(key).hpexpire(key, ttl, *fields, nx: nx, xx: xx, gt: gt, lt: lt)
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
def hpttl(key, *fields)
|
|
1113
|
+
node_for(key).hpttl(key, *fields)
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1116
|
+
# Register a fieldset on every ring node. Fieldsets are scoped to a physical
|
|
1117
|
+
# connection, so every node that may receive an `himport_set` for a key it
|
|
1118
|
+
# owns needs the fieldset on its own connection.
|
|
1119
|
+
def himport_prepare(fieldset_name, *fields)
|
|
1120
|
+
fields.flatten!(1)
|
|
1121
|
+
results = on_each_node(:himport_prepare, fieldset_name, fields)
|
|
1122
|
+
@himport_fieldsets[fieldset_name.to_s] = fields.dup.freeze
|
|
1123
|
+
results
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
# Create or fully replace a hash from a fieldset prepared on the key's node.
|
|
1127
|
+
def himport_set(key, fieldset_name, *values)
|
|
1128
|
+
values.flatten!(1)
|
|
1129
|
+
node_for(key).himport_set(key, fieldset_name, values)
|
|
1130
|
+
end
|
|
1131
|
+
|
|
1132
|
+
# Remove a fieldset from every ring node.
|
|
1133
|
+
def himport_discard(fieldset_name)
|
|
1134
|
+
results = on_each_node(:himport_discard, fieldset_name)
|
|
1135
|
+
@himport_fieldsets.delete(fieldset_name.to_s)
|
|
1136
|
+
results
|
|
1137
|
+
end
|
|
1138
|
+
|
|
1139
|
+
# Remove all fieldsets from every ring node.
|
|
1140
|
+
def himport_discard_all
|
|
1141
|
+
results = on_each_node(:himport_discard_all)
|
|
1142
|
+
@himport_fieldsets.clear
|
|
1143
|
+
results
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# Add one or more geospatial items to a sorted set.
|
|
1147
|
+
def geoadd(key, *member, **options)
|
|
1148
|
+
node_for(key).geoadd(key, *member, **options)
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
# Get geohash strings representing the position of one or more members.
|
|
1152
|
+
def geohash(key, member)
|
|
1153
|
+
node_for(key).geohash(key, member)
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
# Get longitude and latitude of one or more members of a geospatial index.
|
|
1157
|
+
def geopos(key, member)
|
|
1158
|
+
node_for(key).geopos(key, member)
|
|
1159
|
+
end
|
|
1160
|
+
|
|
1161
|
+
# Get the distance between two members of a geospatial index.
|
|
1162
|
+
def geodist(key, member1, member2, unit = 'm')
|
|
1163
|
+
node_for(key).geodist(key, member1, member2, unit)
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
# Query a geospatial index for members within a given radius from a point.
|
|
1167
|
+
def georadius(*args, **geoptions)
|
|
1168
|
+
key = args.first
|
|
1169
|
+
node_for(key).georadius(*args, **geoptions)
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
# Query a geospatial index for members within a given radius from an existing member.
|
|
1173
|
+
def georadiusbymember(*args, **geoptions)
|
|
1174
|
+
key = args.first
|
|
1175
|
+
node_for(key).georadiusbymember(*args, **geoptions)
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
# Search a geospatial index for members within a given shape from a center point.
|
|
1179
|
+
def geosearch(key, **options)
|
|
1180
|
+
node_for(key).geosearch(key, **options)
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
# Like #geosearch, but stores the result in a destination key.
|
|
1184
|
+
# Destination and source must hash to the same node; use a key tag to ensure that.
|
|
1185
|
+
def geosearchstore(destination, source, **options)
|
|
1186
|
+
ensure_same_node(:geosearchstore, [destination, source]) do |node|
|
|
1187
|
+
node.geosearchstore(destination, source, **options)
|
|
1188
|
+
end
|
|
1189
|
+
end
|
|
1190
|
+
|
|
921
1191
|
# Post a message to a channel.
|
|
922
1192
|
def publish(channel, message)
|
|
923
1193
|
node_for(channel).publish(channel, message)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "redis-client"
|
|
4
|
+
require "redis/version"
|
|
5
|
+
|
|
6
|
+
class Redis
|
|
7
|
+
# Reports `redis-rb` as the connecting library via `CLIENT SETINFO`.
|
|
8
|
+
#
|
|
9
|
+
# `redis-client` builds its own `LIB-NAME` as `redis-client(<driver_info>)` and always
|
|
10
|
+
# reports its own version as `LIB-VER`, which would identify `redis-client` rather than
|
|
11
|
+
# `redis-rb` to the server. We append a second pair of `CLIENT SETINFO` commands, which wins
|
|
12
|
+
# because the command is last-write-wins, so that `CLIENT LIST` / `CLIENT INFO` report
|
|
13
|
+
# `lib-name=redis-rb` and `lib-ver=<Redis::VERSION>`.
|
|
14
|
+
#
|
|
15
|
+
# Libraries built on top of `redis-rb` can still identify themselves by passing `driver_info`,
|
|
16
|
+
# in which case they are reported inside the parentheses, e.g. `lib-name=redis-rb(my-gem_v1.0.0)`.
|
|
17
|
+
# The official convention for such suffixes is `<name>_v<version>` (matching
|
|
18
|
+
# `(?<custom-name>[ -~]+)[ -~]v(?<custom-version>[\d\.]+)`), with `;` delimiting multiple
|
|
19
|
+
# suffixes and parentheses avoided inside them — see
|
|
20
|
+
# https://redis.io/docs/latest/commands/client-setinfo/. Passing `driver_info: false` disables
|
|
21
|
+
# `CLIENT SETINFO` entirely, for peers that can't tolerate unknown commands during the handshake
|
|
22
|
+
# (e.g. some proxies).
|
|
23
|
+
#
|
|
24
|
+
# Applications that use `redis-client` directly are left untouched: the override only applies
|
|
25
|
+
# to configurations whose `driver_info` was built by this module.
|
|
26
|
+
module LibIdentity
|
|
27
|
+
# Follows the official suffix convention, so that the transient pair `redis-client` itself
|
|
28
|
+
# builds from this value (`lib-name=redis-client(redis-rb_v<version>)`) is well-formed too.
|
|
29
|
+
MARKER = "redis-rb_v"
|
|
30
|
+
LIB_NAME = "redis-rb"
|
|
31
|
+
SEPARATOR = ";"
|
|
32
|
+
# Interpolated strings are not frozen by the magic comment; explicit freezes keep these
|
|
33
|
+
# constants Ractor-shareable (lazy module ivars here would raise Ractor::IsolationError).
|
|
34
|
+
OWN_INFO = "#{MARKER}#{Redis::VERSION}".freeze
|
|
35
|
+
OWN_PREFIX = "#{OWN_INFO}#{SEPARATOR}".freeze
|
|
36
|
+
# `CLIENT SETINFO` values must be printable ASCII with no spaces — the server rejects anything
|
|
37
|
+
# else, and `redis-client` swallows that error, which would silently leave `lib-name` empty.
|
|
38
|
+
# Parentheses are excluded as well: they delimit the suffix in `lib-name=redis-rb(<suffix>)`,
|
|
39
|
+
# so a suffix containing them would corrupt the reported name.
|
|
40
|
+
INVALID_CHARS = /(?:[^!-~]|[()])+/
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
# Combines redis-rb's own identity with the `driver_info` a downstream library passed in,
|
|
44
|
+
# so that both can be recovered when the connection prelude is built. `false` is passed
|
|
45
|
+
# through untouched and, being falsy, turns `CLIENT SETINFO` off wholesale (`redis-client`
|
|
46
|
+
# gates its own commands on `driver_info` too). Any other non-String, non-Array value is
|
|
47
|
+
# also passed through, leaving `redis-client` to reject it.
|
|
48
|
+
def driver_info(downstream = nil)
|
|
49
|
+
case downstream
|
|
50
|
+
when nil then OWN_INFO
|
|
51
|
+
when String then combine(downstream)
|
|
52
|
+
when Array then combine(downstream.join(SEPARATOR))
|
|
53
|
+
else downstream
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# True only for values `driver_info` produced: exactly our own identity, or our identity
|
|
58
|
+
# followed by a downstream library's. Matching on the bare `MARKER` prefix instead would
|
|
59
|
+
# also capture a direct `redis-client` user whose own `driver_info` merely starts with
|
|
60
|
+
# `redis-rb_v` (say `redis-rb_viewer-1.0`) and would silently discard their identity.
|
|
61
|
+
def ours?(info)
|
|
62
|
+
info.is_a?(String) && (info == OWN_INFO || info.start_with?(OWN_PREFIX))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def setinfo_commands(info)
|
|
66
|
+
[
|
|
67
|
+
["CLIENT", "SETINFO", "LIB-NAME", lib_name(info)].freeze,
|
|
68
|
+
["CLIENT", "SETINFO", "LIB-VER", Redis::VERSION].freeze,
|
|
69
|
+
].freeze
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def combine(downstream)
|
|
75
|
+
downstream = downstream.scrub(" ") # invalid byte sequences would make gsub raise
|
|
76
|
+
.gsub(/\A#{INVALID_CHARS}|#{INVALID_CHARS}\z/o, "")
|
|
77
|
+
.gsub(INVALID_CHARS, "_")
|
|
78
|
+
downstream.empty? ? OWN_INFO : "#{OWN_PREFIX}#{downstream}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def lib_name(info)
|
|
82
|
+
downstream = info == OWN_INFO ? nil : info.delete_prefix(OWN_PREFIX)
|
|
83
|
+
downstream.nil? || downstream.empty? ? LIB_NAME : "#{LIB_NAME}(#{downstream})".freeze
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def connection_prelude
|
|
88
|
+
prelude = super
|
|
89
|
+
info = driver_info
|
|
90
|
+
return prelude unless LibIdentity.ours?(info)
|
|
91
|
+
|
|
92
|
+
# `CLIENT SETINFO` is last-write-wins, so appending after the commands `redis-client`
|
|
93
|
+
# already built overrides them, without having to recognise or remove them.
|
|
94
|
+
(prelude + LibIdentity.setinfo_commands(info)).freeze
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Prepended to the concrete config classes rather than to `RedisClient::Config::Common`, so that
|
|
100
|
+
# the two `redis-client` entry points are covered without altering the shared module for anything
|
|
101
|
+
# else that includes it. `RedisClient::SentinelConfig` is NOT a `RedisClient::Config` subclass, so
|
|
102
|
+
# both lines are required. `RedisClient::Cluster::Node::Config` does subclass `RedisClient::Config`
|
|
103
|
+
# and calls `super` in its own `connection_prelude`, so cluster nodes are covered by the first.
|
|
104
|
+
RedisClient::Config.prepend(Redis::LibIdentity)
|
|
105
|
+
RedisClient::SentinelConfig.prepend(Redis::LibIdentity)
|