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.
@@ -220,13 +220,20 @@ class Redis
220
220
  # @param [Hash] options
221
221
  # - `:match => String`: only return keys matching the pattern
222
222
  # - `:count => Integer`: return count keys at most per iteration
223
+ # - `:novalues => Boolean`: whether or not to include values in the output (default: false)
223
224
  #
224
- # @return [String, Array<[String, String]>] the next cursor and all found keys
225
+ # @return [String, Array<[String, String]>, Array<String>] the next cursor and all found keys
226
+ # - when `:novalues` is false: [cursor, [[field1, value1], [field2, value2], ...]]
227
+ # - when `:novalues` is true: [cursor, [field1, field2, ...]]
225
228
  #
226
229
  # See the [Redis Server HSCAN documentation](https://redis.io/docs/latest/commands/hscan/) for further details
227
230
  def hscan(key, cursor, **options)
228
231
  _scan(:hscan, cursor, [key], **options) do |reply|
229
- [reply[0], reply[1].each_slice(2).to_a]
232
+ if options[:novalues]
233
+ reply
234
+ else
235
+ [reply[0], reply[1].each_slice(2).to_a]
236
+ end
230
237
  end
231
238
  end
232
239
 
@@ -239,6 +246,7 @@ class Redis
239
246
  # @param [Hash] options
240
247
  # - `:match => String`: only return keys matching the pattern
241
248
  # - `:count => Integer`: return count keys at most per iteration
249
+ # - `:novalues => Boolean`: whether or not to include values in the output (default: false)
242
250
  #
243
251
  # @return [Enumerator] an enumerator for all found keys
244
252
  #
@@ -253,6 +261,162 @@ class Redis
253
261
  break if cursor == "0"
254
262
  end
255
263
  end
264
+
265
+ # Sets the time to live in seconds for one or more fields.
266
+ #
267
+ # @example
268
+ # redis.hset("hash", "f1", "v1")
269
+ # redis.hexpire("hash", 10, "f1", "f2") # => [1, -2]
270
+ #
271
+ # @param [String] key
272
+ # @param [Integer] ttl
273
+ # @param [Array<String>] fields
274
+ # @return [Array<Integer>] Feedback on if the fields have been updated.
275
+ #
276
+ # See https://redis.io/docs/latest/commands/hexpire/#return-information for array reply.
277
+ def hexpire(key, ttl, *fields)
278
+ send_command([:hexpire, key, ttl, 'FIELDS', fields.length, *fields])
279
+ end
280
+
281
+ # Returns the time to live in seconds for one or more fields.
282
+ #
283
+ # @example
284
+ # redis.hset("hash", "f1", "v1", "f2", "v2")
285
+ # redis.hexpire("hash", 10, "f1") # => [1]
286
+ # redis.httl("hash", "f1", "f2", "f3") # => [10, -1, -2]
287
+ #
288
+ # @param [String] key
289
+ # @param [Array<String>] fields
290
+ # @return [Array<Integer>] Feedback on the TTL of the fields.
291
+ #
292
+ # See https://redis.io/docs/latest/commands/httl/#return-information for array reply.
293
+ def httl(key, *fields)
294
+ send_command([:httl, key, 'FIELDS', fields.length, *fields])
295
+ end
296
+
297
+ # Sets the time to live in milliseconds for one or more fields.
298
+ #
299
+ # @example
300
+ # redis.hset("hash", "f1", "v1")
301
+ # redis.hpexpire("hash", 500, "f1", "f2") # => [1, -2]
302
+ # redis.hpexpire("hash", 500, "f1", "f2", nx: true) # => [0, -2]
303
+ #
304
+ # @param [String] key
305
+ # @param [Integer] ttl
306
+ # @param [Hash] options
307
+ # - `:nx => true`: Set expiry only when the key has no expiry.
308
+ # - `:xx => true`: Set expiry only when the key has an existing expiry.
309
+ # - `:gt => true`: Set expiry only when the new expiry is greater than current one.
310
+ # - `:lt => true`: Set expiry only when the new expiry is less than current one.
311
+ # @param [Array<String>] fields
312
+ # @return [Array<Integer>] Feedback on if the fields have been updated.
313
+ #
314
+ # See https://redis.io/docs/latest/commands/hpexpire/#return-information for array reply.
315
+ def hpexpire(key, ttl, *fields, nx: nil, xx: nil, gt: nil, lt: nil)
316
+ args = [:hpexpire, key, ttl]
317
+ args << "NX" if nx
318
+ args << "XX" if xx
319
+ args << "GT" if gt
320
+ args << "LT" if lt
321
+ args.concat(['FIELDS', fields.length, *fields])
322
+
323
+ send_command(args)
324
+ end
325
+
326
+ # Returns the time to live in milliseconds for one or more fields.
327
+ #
328
+ # @example
329
+ # redis.hset("hash", "f1", "v1", "f2", "v2")
330
+ # redis.hpexpire("hash", 500, "f1") # => [1]
331
+ # redis.hpttl("hash", "f1", "f2", "f3") # => [500, -1, -2]
332
+ #
333
+ # @param [String] key
334
+ # @param [Array<String>] fields
335
+ # @return [Array<Integer>] Feedback on the TTL of the fields.
336
+ #
337
+ # See https://redis.io/docs/latest/commands/hpttl/#return-information for array reply.
338
+ def hpttl(key, *fields)
339
+ send_command([:hpttl, key, 'FIELDS', fields.length, *fields])
340
+ end
341
+
342
+ # Register an ordered list of hash field names under +fieldset_name+ for
343
+ # use by subsequent #himport_set calls on the same connection.
344
+ #
345
+ # Fieldsets are server-side session state scoped to the current physical
346
+ # connection: they vanish on disconnect or RESET and are invisible to
347
+ # other connections. See the README "Bulk hash ingestion (HIMPORT)"
348
+ # section for connection-scoping guidance. Re-preparing an existing
349
+ # +fieldset_name+ silently replaces it. Field order is preserved as
350
+ # given; it defines the positional pairing used by #himport_set.
351
+ #
352
+ # @note HIMPORT support is experimental: the client API may change in a
353
+ # future minor release without a major version bump.
354
+ #
355
+ # @example
356
+ # redis.himport_prepare("shared", ["name", "email", "age"])
357
+ # # => "OK"
358
+ #
359
+ # @param [String] fieldset_name name used by later SET and DISCARD calls
360
+ # @param [String, Array<String>] fields one or more field names
361
+ # @return [String] `"OK"`
362
+ # @api experimental
363
+ def himport_prepare(fieldset_name, *fields)
364
+ fields.flatten!(1)
365
+ raise ArgumentError, "fields must not be empty" if fields.empty?
366
+
367
+ send_command([:himport, "PREPARE", fieldset_name].concat(fields))
368
+ end
369
+
370
+ # Create or fully replace the hash at +key+ using the field list
371
+ # registered under +fieldset_name+ on this connection. Values pair
372
+ # positionally with the fields given to #himport_prepare; the value
373
+ # count must equal the field count.
374
+ #
375
+ # The fieldset must exist on the executing connection, otherwise the
376
+ # server replies with a "no such fieldset" error.
377
+ #
378
+ # @note HIMPORT support is experimental: the client API may change in a
379
+ # future minor release without a major version bump.
380
+ #
381
+ # @example
382
+ # redis.himport_set("shared:1", "shared", ["alice", "alice@example.com", "25"])
383
+ # # => "OK"
384
+ #
385
+ # @param [String] key hash key to create or overwrite
386
+ # @param [String] fieldset_name fieldset previously prepared on this connection
387
+ # @param [String, Array<String>] values one or more values, order preserved
388
+ # @return [String] `"OK"`
389
+ # @api experimental
390
+ def himport_set(key, fieldset_name, *values)
391
+ values.flatten!(1)
392
+ raise ArgumentError, "values must not be empty" if values.empty?
393
+
394
+ send_command([:himport, "SET", key, fieldset_name].concat(values))
395
+ end
396
+
397
+ # Remove +fieldset_name+ from this connection's session. Keys already
398
+ # written through the fieldset are not affected.
399
+ #
400
+ # @note HIMPORT support is experimental: the client API may change in a
401
+ # future minor release without a major version bump.
402
+ #
403
+ # @param [String] fieldset_name
404
+ # @return [Integer] `1` if the fieldset was removed, `0` if it did not exist
405
+ # @api experimental
406
+ def himport_discard(fieldset_name)
407
+ send_command([:himport, "DISCARD", fieldset_name])
408
+ end
409
+
410
+ # Remove all fieldsets from this connection's session.
411
+ #
412
+ # @note HIMPORT support is experimental: the client API may change in a
413
+ # future minor release without a major version bump.
414
+ #
415
+ # @return [Integer] number of fieldsets removed
416
+ # @api experimental
417
+ def himport_discard_all
418
+ send_command([:himport, "DISCARDALL"])
419
+ end
256
420
  end
257
421
  end
258
422
  end
@@ -261,6 +261,9 @@ class Redis
261
261
  # @param [String, Array<String>] keys
262
262
  # @return [Integer] number of keys that were unlinked
263
263
  def unlink(*keys)
264
+ keys.flatten!(1)
265
+ return 0 if keys.empty?
266
+
264
267
  send_command([:unlink] + keys)
265
268
  end
266
269
 
@@ -444,13 +447,14 @@ class Redis
444
447
 
445
448
  private
446
449
 
447
- def _scan(command, cursor, args, match: nil, count: nil, type: nil, &block)
450
+ def _scan(command, cursor, args, match: nil, count: nil, type: nil, novalues: false, &block)
448
451
  # SSCAN/ZSCAN/HSCAN already prepend the key to +args+.
449
452
 
450
453
  args << cursor
451
454
  args << "MATCH" << match if match
452
455
  args << "COUNT" << Integer(count) if count
453
456
  args << "TYPE" << type if type
457
+ args << "NOVALUES" if novalues
454
458
 
455
459
  send_command([command] + args, &block)
456
460
  end
@@ -30,6 +30,84 @@ class Redis
30
30
  send_command([:lmove, source, destination, where_source, where_destination])
31
31
  end
32
32
 
33
+ # Remove multiple elements from the head/tail of a list, append/prepend
34
+ # them to another list and return them.
35
+ #
36
+ # @example Move a single element (still an array reply)
37
+ # redis.lmovem("foo", "bar", "LEFT", "LEFT")
38
+ # # => ["s1"]
39
+ # @example Move up to 3 elements, pushed one-by-one (block order reversed)
40
+ # redis.lmovem("foo", "bar", "LEFT", "LEFT", count: 3, order: "OBO")
41
+ # # => ["s3", "s2", "s1"]
42
+ # @example Move exactly 2 elements, preserving their relative order
43
+ # redis.lmovem("foo", "bar", "LEFT", "RIGHT", exactly: 2, order: "BULK")
44
+ # # => ["s1", "s2"]
45
+ #
46
+ # @param [String] source source key
47
+ # @param [String] destination destination key
48
+ # @param [String, Symbol] where_source from where to remove elements from the source list
49
+ # e.g. 'LEFT' - from head, 'RIGHT' - from tail
50
+ # @param [String, Symbol] where_destination where to push elements to the destination list
51
+ # e.g. 'LEFT' - to head, 'RIGHT' - to tail
52
+ # @param [Integer] count move up to `count` elements, fewer when the source holds fewer
53
+ # @param [Integer] exactly move exactly `exactly` elements; when the source holds fewer,
54
+ # nothing is moved
55
+ # @param [String, Symbol] order ordering at the destination, required together with
56
+ # `count` or `exactly`:
57
+ # - when `'OBO'` - push each element as popped, so the moved block order is reversed
58
+ # - when `'BULK'` - preserve the original relative order of the moved elements
59
+ #
60
+ # @return [nil, Array<String>] the moved elements in destination order, or nil when
61
+ # nothing was moved
62
+ def lmovem(source, destination, where_source, where_destination, count: nil, exactly: nil, order: nil)
63
+ where_source, where_destination = _normalize_move_wheres(where_source, where_destination)
64
+
65
+ args = [:lmovem, source, destination, where_source, where_destination]
66
+ args.concat(_movem_amount_args(count, exactly, order))
67
+
68
+ send_command(args)
69
+ end
70
+
71
+ # Remove multiple elements from the head/tail of a list, append/prepend
72
+ # them to another list and return them; or block until the request can
73
+ # be satisfied or the timeout expires.
74
+ #
75
+ # @example Move up to 3 elements as soon as any are available
76
+ # redis.blmovem("foo", "bar", "LEFT", "LEFT", timeout: 1, count: 3, order: "BULK")
77
+ # # => ["s1", "s2", "s3"]
78
+ # @example Block until the source holds at least 2 elements
79
+ # redis.blmovem("foo", "bar", "LEFT", "RIGHT", timeout: 1, exactly: 2, order: "OBO")
80
+ # # => nil on timeout
81
+ #
82
+ # @param [String] source source key
83
+ # @param [String] destination destination key
84
+ # @param [String, Symbol] where_source from where to remove elements from the source list
85
+ # e.g. 'LEFT' - from head, 'RIGHT' - from tail
86
+ # @param [String, Symbol] where_destination where to push elements to the destination list
87
+ # e.g. 'LEFT' - to head, 'RIGHT' - to tail
88
+ # @param [Float, Integer] timeout seconds to block, 0 blocks indefinitely
89
+ # @param [Integer] count move up to `count` elements as soon as at least one is available
90
+ # @param [Integer] exactly move exactly `exactly` elements, blocking until the source
91
+ # holds that many
92
+ # @param [String, Symbol] order ordering at the destination, required together with
93
+ # `count` or `exactly`:
94
+ # - when `'OBO'` - push each element as popped, so the moved block order is reversed
95
+ # - when `'BULK'` - preserve the original relative order of the moved elements
96
+ #
97
+ # @return [nil, Array<String>] the moved elements in destination order, or nil when the
98
+ # timeout expired and nothing was moved
99
+ #
100
+ # @see #lmovem
101
+ def blmovem(source, destination, where_source, where_destination, timeout: 0, count: nil, exactly: nil,
102
+ order: nil)
103
+ where_source, where_destination = _normalize_move_wheres(where_source, where_destination)
104
+
105
+ command = [:blmovem, source, destination, where_source, where_destination, timeout]
106
+ command.concat(_movem_amount_args(count, exactly, order))
107
+
108
+ send_blocking_command(command, timeout)
109
+ end
110
+
33
111
  # Remove the first/last element in a list and append/prepend it
34
112
  # to another list and return it, or block until one is available.
35
113
  #
@@ -320,6 +398,21 @@ class Redis
320
398
  send_blocking_command(command, timeout, &blk)
321
399
  end
322
400
 
401
+ def _movem_amount_args(count, exactly, order)
402
+ raise ArgumentError, "Pick either count or exactly, not both" if count && exactly
403
+
404
+ if count || exactly
405
+ order = order.to_s.upcase
406
+ raise ArgumentError, "order must be 'OBO' or 'BULK'" if order != "OBO" && order != "BULK"
407
+
408
+ [count ? "COUNT" : "EXACTLY", Integer(count || exactly), order]
409
+ elsif order
410
+ raise ArgumentError, "order requires count or exactly"
411
+ else
412
+ []
413
+ end
414
+ end
415
+
323
416
  def _normalize_move_wheres(where_source, where_destination)
324
417
  where_source = where_source.to_s.upcase
325
418
  where_destination = where_destination.to_s.upcase