familia 2.0.0.pre26 → 2.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.rst +49 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -3
- data/docs/guides/feature-encrypted-fields.md +1 -1
- data/docs/guides/feature-expiration.md +1 -1
- data/docs/guides/feature-quantization.md +1 -1
- data/docs/overview.md +7 -7
- data/docs/reference/api-technical.md +103 -7
- data/familia.gemspec +1 -2
- data/lib/familia/data_type/types/hashkey.rb +238 -0
- data/lib/familia/data_type/types/listkey.rb +110 -4
- data/lib/familia/data_type/types/sorted_set.rb +365 -0
- data/lib/familia/data_type/types/stringkey.rb +139 -0
- data/lib/familia/data_type/types/unsorted_set.rb +122 -2
- data/lib/familia/version.rb +1 -1
- metadata +2 -27
- data/docs/migrating/v2.0.0-pre.md +0 -84
- data/docs/migrating/v2.0.0-pre11.md +0 -253
- data/docs/migrating/v2.0.0-pre12.md +0 -306
- data/docs/migrating/v2.0.0-pre13.md +0 -95
- data/docs/migrating/v2.0.0-pre14.md +0 -37
- data/docs/migrating/v2.0.0-pre18.md +0 -58
- data/docs/migrating/v2.0.0-pre19.md +0 -197
- data/docs/migrating/v2.0.0-pre22.md +0 -241
- data/docs/migrating/v2.0.0-pre5.md +0 -131
- data/docs/migrating/v2.0.0-pre6.md +0 -154
- data/docs/migrating/v2.0.0-pre7.md +0 -222
|
@@ -41,12 +41,32 @@ module Familia
|
|
|
41
41
|
end
|
|
42
42
|
alias prepend unshift
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
# Removes and returns the last element(s) from the list
|
|
45
|
+
# @param count [Integer, nil] Number of elements to pop (Redis 6.2+)
|
|
46
|
+
# @return [Object, Array<Object>, nil] Single element or array if count specified
|
|
47
|
+
def pop(count = nil)
|
|
48
|
+
if count
|
|
49
|
+
result = dbclient.rpop(dbkey, count)
|
|
50
|
+
return nil if result.nil?
|
|
51
|
+
|
|
52
|
+
deserialize_values(*result)
|
|
53
|
+
else
|
|
54
|
+
deserialize_value dbclient.rpop(dbkey)
|
|
55
|
+
end
|
|
46
56
|
end
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
# Removes and returns the first element(s) from the list
|
|
59
|
+
# @param count [Integer, nil] Number of elements to shift (Redis 6.2+)
|
|
60
|
+
# @return [Object, Array<Object>, nil] Single element or array if count specified
|
|
61
|
+
def shift(count = nil)
|
|
62
|
+
if count
|
|
63
|
+
result = dbclient.lpop(dbkey, count)
|
|
64
|
+
return nil if result.nil?
|
|
65
|
+
|
|
66
|
+
deserialize_values(*result)
|
|
67
|
+
else
|
|
68
|
+
deserialize_value dbclient.lpop(dbkey)
|
|
69
|
+
end
|
|
50
70
|
end
|
|
51
71
|
|
|
52
72
|
def [](idx, count = nil)
|
|
@@ -135,6 +155,92 @@ module Familia
|
|
|
135
155
|
deserialize_value dbclient.lindex(dbkey, idx)
|
|
136
156
|
end
|
|
137
157
|
|
|
158
|
+
# Trims the list to the specified range
|
|
159
|
+
# @param start [Integer] Start index (0-based, negative counts from end)
|
|
160
|
+
# @param stop [Integer] End index (inclusive, negative counts from end)
|
|
161
|
+
# @return [String] "OK" on success
|
|
162
|
+
def trim(start, stop)
|
|
163
|
+
dbclient.ltrim dbkey, start, stop
|
|
164
|
+
end
|
|
165
|
+
alias ltrim trim
|
|
166
|
+
|
|
167
|
+
# Sets the element at the specified index
|
|
168
|
+
# @param index [Integer] Index to set (0-based, negative counts from end)
|
|
169
|
+
# @param value The value to set
|
|
170
|
+
# @return [String] "OK" on success
|
|
171
|
+
# @raise [Redis::CommandError] if index is out of range
|
|
172
|
+
def set(index, value)
|
|
173
|
+
result = dbclient.lset dbkey, index, serialize_value(value)
|
|
174
|
+
update_expiration
|
|
175
|
+
result
|
|
176
|
+
end
|
|
177
|
+
alias lset set
|
|
178
|
+
|
|
179
|
+
# Inserts an element before or after a pivot element
|
|
180
|
+
# @param position [:before, :after] Where to insert relative to pivot
|
|
181
|
+
# @param pivot The pivot element to search for
|
|
182
|
+
# @param value The value to insert
|
|
183
|
+
# @return [Integer] Length of list after insert, or -1 if pivot not found
|
|
184
|
+
def insert(position, pivot, value)
|
|
185
|
+
pos = case position
|
|
186
|
+
when :before, 'BEFORE' then 'BEFORE'
|
|
187
|
+
when :after, 'AFTER' then 'AFTER'
|
|
188
|
+
else
|
|
189
|
+
raise ArgumentError, "position must be :before or :after, got #{position.inspect}"
|
|
190
|
+
end
|
|
191
|
+
result = dbclient.linsert dbkey, pos, serialize_value(pivot), serialize_value(value)
|
|
192
|
+
update_expiration if result.positive?
|
|
193
|
+
result
|
|
194
|
+
end
|
|
195
|
+
alias linsert insert
|
|
196
|
+
|
|
197
|
+
# Moves an element from this list to another list atomically
|
|
198
|
+
# @param destination [ListKey, String] Destination list (ListKey or key string)
|
|
199
|
+
# @param wherefrom [:left, :right] Which end to pop from source
|
|
200
|
+
# @param whereto [:left, :right] Which end to push to destination
|
|
201
|
+
# @return [Object, nil] The moved element, or nil if source is empty
|
|
202
|
+
def move(destination, wherefrom, whereto)
|
|
203
|
+
dest_key = destination.respond_to?(:dbkey) ? destination.dbkey : destination
|
|
204
|
+
from = wherefrom.to_s.upcase
|
|
205
|
+
to = whereto.to_s.upcase
|
|
206
|
+
|
|
207
|
+
unless %w[LEFT RIGHT].include?(from) && %w[LEFT RIGHT].include?(to)
|
|
208
|
+
raise ArgumentError, 'wherefrom and whereto must be :left or :right'
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
result = dbclient.lmove dbkey, dest_key, from, to
|
|
212
|
+
deserialize_value result
|
|
213
|
+
end
|
|
214
|
+
alias lmove move
|
|
215
|
+
|
|
216
|
+
# Pushes values only if the list already exists
|
|
217
|
+
# @param values Values to push to the tail of the list
|
|
218
|
+
# @return [Integer] Length of list after push, or 0 if list doesn't exist
|
|
219
|
+
def pushx(*values)
|
|
220
|
+
return 0 if values.empty?
|
|
221
|
+
|
|
222
|
+
result = values.flatten.compact.reduce(0) do |len, v|
|
|
223
|
+
dbclient.rpushx dbkey, serialize_value(v)
|
|
224
|
+
end
|
|
225
|
+
update_expiration if result.positive?
|
|
226
|
+
result
|
|
227
|
+
end
|
|
228
|
+
alias rpushx pushx
|
|
229
|
+
|
|
230
|
+
# Pushes values to the head only if the list already exists
|
|
231
|
+
# @param values Values to push to the head of the list
|
|
232
|
+
# @return [Integer] Length of list after push, or 0 if list doesn't exist
|
|
233
|
+
def unshiftx(*values)
|
|
234
|
+
return 0 if values.empty?
|
|
235
|
+
|
|
236
|
+
result = values.flatten.compact.reduce(0) do |len, v|
|
|
237
|
+
dbclient.lpushx dbkey, serialize_value(v)
|
|
238
|
+
end
|
|
239
|
+
update_expiration if result.positive?
|
|
240
|
+
result
|
|
241
|
+
end
|
|
242
|
+
alias lpushx unshiftx
|
|
243
|
+
|
|
138
244
|
def first
|
|
139
245
|
at 0
|
|
140
246
|
end
|
|
@@ -303,9 +303,374 @@ module Familia
|
|
|
303
303
|
at(-1)
|
|
304
304
|
end
|
|
305
305
|
|
|
306
|
+
# Removes and returns the member(s) with the lowest score(s).
|
|
307
|
+
#
|
|
308
|
+
# @param count [Integer] Number of members to pop (default: 1)
|
|
309
|
+
# @return [Array, nil] Array of [member, score] pairs, or single pair if count=1,
|
|
310
|
+
# or nil if set is empty
|
|
311
|
+
#
|
|
312
|
+
# @example Pop single lowest-scoring member
|
|
313
|
+
# zset.popmin #=> ["member1", 1.0]
|
|
314
|
+
#
|
|
315
|
+
# @example Pop multiple lowest-scoring members
|
|
316
|
+
# zset.popmin(3) #=> [["member1", 1.0], ["member2", 2.0], ["member3", 3.0]]
|
|
317
|
+
#
|
|
318
|
+
def popmin(count = 1)
|
|
319
|
+
result = dbclient.zpopmin(dbkey, count)
|
|
320
|
+
return nil if result.nil? || result.empty?
|
|
321
|
+
|
|
322
|
+
update_expiration
|
|
323
|
+
|
|
324
|
+
if count == 1 && result.is_a?(Array) && result.length == 2 && !result[0].is_a?(Array)
|
|
325
|
+
# Single result: [member, score]
|
|
326
|
+
[deserialize_value(result[0]), result[1].to_f]
|
|
327
|
+
else
|
|
328
|
+
# Multiple results: [[member, score], ...]
|
|
329
|
+
result.map { |member, score| [deserialize_value(member), score.to_f] }
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Removes and returns the member(s) with the highest score(s).
|
|
334
|
+
#
|
|
335
|
+
# @param count [Integer] Number of members to pop (default: 1)
|
|
336
|
+
# @return [Array, nil] Array of [member, score] pairs, or single pair if count=1,
|
|
337
|
+
# or nil if set is empty
|
|
338
|
+
#
|
|
339
|
+
# @example Pop single highest-scoring member
|
|
340
|
+
# zset.popmax #=> ["member1", 100.0]
|
|
341
|
+
#
|
|
342
|
+
# @example Pop multiple highest-scoring members
|
|
343
|
+
# zset.popmax(3) #=> [["member3", 100.0], ["member2", 90.0], ["member1", 80.0]]
|
|
344
|
+
#
|
|
345
|
+
def popmax(count = 1)
|
|
346
|
+
result = dbclient.zpopmax(dbkey, count)
|
|
347
|
+
return nil if result.nil? || result.empty?
|
|
348
|
+
|
|
349
|
+
update_expiration
|
|
350
|
+
|
|
351
|
+
if count == 1 && result.is_a?(Array) && result.length == 2 && !result[0].is_a?(Array)
|
|
352
|
+
# Single result: [member, score]
|
|
353
|
+
[deserialize_value(result[0]), result[1].to_f]
|
|
354
|
+
else
|
|
355
|
+
# Multiple results: [[member, score], ...]
|
|
356
|
+
result.map { |member, score| [deserialize_value(member), score.to_f] }
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Counts members within a score range.
|
|
361
|
+
#
|
|
362
|
+
# @param min [Numeric, String] Minimum score (use '-inf' for unbounded)
|
|
363
|
+
# @param max [Numeric, String] Maximum score (use '+inf' for unbounded)
|
|
364
|
+
# @return [Integer] Number of members with scores in the range
|
|
365
|
+
#
|
|
366
|
+
# @example Count members with scores between 10 and 100
|
|
367
|
+
# zset.score_count(10, 100) #=> 5
|
|
368
|
+
#
|
|
369
|
+
# @example Count members with scores up to 50
|
|
370
|
+
# zset.score_count('-inf', 50) #=> 3
|
|
371
|
+
#
|
|
372
|
+
def score_count(min, max)
|
|
373
|
+
dbclient.zcount(dbkey, min, max)
|
|
374
|
+
end
|
|
375
|
+
alias zcount score_count
|
|
376
|
+
|
|
377
|
+
# Gets scores for multiple members at once.
|
|
378
|
+
#
|
|
379
|
+
# @param members [Array<Object>] Members to get scores for
|
|
380
|
+
# @return [Array<Float, nil>] Scores for each member (nil if member doesn't exist)
|
|
381
|
+
#
|
|
382
|
+
# @example Get scores for multiple members
|
|
383
|
+
# zset.mscore('member1', 'member2', 'member3') #=> [1.0, 2.0, nil]
|
|
384
|
+
#
|
|
385
|
+
def mscore(*members)
|
|
386
|
+
return [] if members.empty?
|
|
387
|
+
|
|
388
|
+
serialized = members.map { |m| serialize_value(m) }
|
|
389
|
+
result = dbclient.zmscore(dbkey, *serialized)
|
|
390
|
+
result.map { |s| s&.to_f }
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Returns the union of this sorted set with other sorted sets.
|
|
394
|
+
#
|
|
395
|
+
# @param other_sets [Array<SortedSet, String>] Other sorted sets or key names
|
|
396
|
+
# @param weights [Array<Numeric>, nil] Multiplication factors for each set's scores
|
|
397
|
+
# @param aggregate [Symbol, nil] How to aggregate scores (:sum, :min, :max)
|
|
398
|
+
# @return [Array] Array of members (or [member, score] pairs with withscores)
|
|
399
|
+
#
|
|
400
|
+
# @example Union of two sorted sets
|
|
401
|
+
# zset.union(other_zset) #=> ["member1", "member2", "member3"]
|
|
402
|
+
#
|
|
403
|
+
# @example Union with weighted scores
|
|
404
|
+
# zset.union(other_zset, weights: [1, 2])
|
|
405
|
+
#
|
|
406
|
+
# @example Union with score aggregation
|
|
407
|
+
# zset.union(other_zset, aggregate: :max)
|
|
408
|
+
#
|
|
409
|
+
def union(*other_sets, weights: nil, aggregate: nil, withscores: false)
|
|
410
|
+
keys = [dbkey] + resolve_set_keys(other_sets)
|
|
411
|
+
opts = build_set_operation_opts(weights: weights, aggregate: aggregate, withscores: withscores)
|
|
412
|
+
|
|
413
|
+
result = dbclient.zunion(*keys, **opts)
|
|
414
|
+
process_set_operation_result(result, withscores: withscores)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Returns the intersection of this sorted set with other sorted sets.
|
|
418
|
+
#
|
|
419
|
+
# @param other_sets [Array<SortedSet, String>] Other sorted sets or key names
|
|
420
|
+
# @param weights [Array<Numeric>, nil] Multiplication factors for each set's scores
|
|
421
|
+
# @param aggregate [Symbol, nil] How to aggregate scores (:sum, :min, :max)
|
|
422
|
+
# @return [Array] Array of members (or [member, score] pairs with withscores)
|
|
423
|
+
#
|
|
424
|
+
# @example Intersection of two sorted sets
|
|
425
|
+
# zset.inter(other_zset) #=> ["common_member"]
|
|
426
|
+
#
|
|
427
|
+
def inter(*other_sets, weights: nil, aggregate: nil, withscores: false)
|
|
428
|
+
keys = [dbkey] + resolve_set_keys(other_sets)
|
|
429
|
+
opts = build_set_operation_opts(weights: weights, aggregate: aggregate, withscores: withscores)
|
|
430
|
+
|
|
431
|
+
result = dbclient.zinter(*keys, **opts)
|
|
432
|
+
process_set_operation_result(result, withscores: withscores)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Returns members in a lexicographical range (requires all members have same score).
|
|
436
|
+
#
|
|
437
|
+
# @param min [String] Minimum lex value (use '-' for unbounded, '[' or '(' prefix for inclusive/exclusive)
|
|
438
|
+
# @param max [String] Maximum lex value (use '+' for unbounded, '[' or '(' prefix for inclusive/exclusive)
|
|
439
|
+
# @param limit [Array<Integer>, nil] [offset, count] for pagination
|
|
440
|
+
# @return [Array] Members in the lexicographical range
|
|
441
|
+
#
|
|
442
|
+
# @example Get members between 'a' and 'z' (inclusive)
|
|
443
|
+
# zset.rangebylex('[a', '[z') #=> ["apple", "banana", "cherry"]
|
|
444
|
+
#
|
|
445
|
+
# @example Get first 10 members starting with 'a'
|
|
446
|
+
# zset.rangebylex('[a', '(b', limit: [0, 10])
|
|
447
|
+
#
|
|
448
|
+
def rangebylex(min, max, limit: nil)
|
|
449
|
+
args = [dbkey, min, max]
|
|
450
|
+
args.push(:limit, *limit) if limit
|
|
451
|
+
|
|
452
|
+
result = dbclient.zrangebylex(*args)
|
|
453
|
+
deserialize_values(*result)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Returns members in reverse lexicographical range.
|
|
457
|
+
#
|
|
458
|
+
# @param max [String] Maximum lex value (use '+' for unbounded)
|
|
459
|
+
# @param min [String] Minimum lex value (use '-' for unbounded)
|
|
460
|
+
# @param limit [Array<Integer>, nil] [offset, count] for pagination
|
|
461
|
+
# @return [Array] Members in reverse lexicographical range
|
|
462
|
+
#
|
|
463
|
+
def revrangebylex(max, min, limit: nil)
|
|
464
|
+
args = [dbkey, max, min]
|
|
465
|
+
args.push(:limit, *limit) if limit
|
|
466
|
+
|
|
467
|
+
result = dbclient.zrevrangebylex(*args)
|
|
468
|
+
deserialize_values(*result)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Removes members in a lexicographical range.
|
|
472
|
+
#
|
|
473
|
+
# @param min [String] Minimum lex value
|
|
474
|
+
# @param max [String] Maximum lex value
|
|
475
|
+
# @return [Integer] Number of members removed
|
|
476
|
+
#
|
|
477
|
+
def remrangebylex(min, max)
|
|
478
|
+
result = dbclient.zremrangebylex(dbkey, min, max)
|
|
479
|
+
update_expiration
|
|
480
|
+
result
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Counts members in a lexicographical range.
|
|
484
|
+
#
|
|
485
|
+
# @param min [String] Minimum lex value
|
|
486
|
+
# @param max [String] Maximum lex value
|
|
487
|
+
# @return [Integer] Number of members in the range
|
|
488
|
+
#
|
|
489
|
+
def lexcount(min, max)
|
|
490
|
+
dbclient.zlexcount(dbkey, min, max)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Returns random member(s) from the sorted set.
|
|
494
|
+
#
|
|
495
|
+
# @param count [Integer, nil] Number of members to return (nil for single member)
|
|
496
|
+
# @param withscores [Boolean] Whether to include scores in result
|
|
497
|
+
# @return [Object, Array, nil] Random member(s), or nil if set is empty
|
|
498
|
+
#
|
|
499
|
+
# @example Get single random member
|
|
500
|
+
# zset.randmember #=> "member1"
|
|
501
|
+
#
|
|
502
|
+
# @example Get 3 random members
|
|
503
|
+
# zset.randmember(3) #=> ["member1", "member2", "member3"]
|
|
504
|
+
#
|
|
505
|
+
# @example Get random member with score
|
|
506
|
+
# zset.randmember(1, withscores: true) #=> [["member1", 1.0]]
|
|
507
|
+
#
|
|
508
|
+
def randmember(count = nil, withscores: false)
|
|
509
|
+
if count.nil?
|
|
510
|
+
result = dbclient.zrandmember(dbkey)
|
|
511
|
+
return nil if result.nil?
|
|
512
|
+
|
|
513
|
+
deserialize_value(result)
|
|
514
|
+
else
|
|
515
|
+
result = if withscores
|
|
516
|
+
dbclient.zrandmember(dbkey, count, withscores: true)
|
|
517
|
+
else
|
|
518
|
+
dbclient.zrandmember(dbkey, count)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
return [] if result.nil? || result.empty?
|
|
522
|
+
|
|
523
|
+
if withscores
|
|
524
|
+
result.map { |member, score| [deserialize_value(member), score.to_f] }
|
|
525
|
+
else
|
|
526
|
+
deserialize_values(*result)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Iterates over members using cursor-based scanning.
|
|
532
|
+
#
|
|
533
|
+
# @param cursor [Integer] Cursor position (0 to start)
|
|
534
|
+
# @param match [String, nil] Pattern to match member names
|
|
535
|
+
# @param count [Integer, nil] Hint for number of elements to return per call
|
|
536
|
+
# @return [Array] [new_cursor, [[member, score], ...]]
|
|
537
|
+
#
|
|
538
|
+
# @example Scan all members
|
|
539
|
+
# cursor = 0
|
|
540
|
+
# loop do
|
|
541
|
+
# cursor, members = zset.scan(cursor)
|
|
542
|
+
# members.each { |member, score| puts "#{member}: #{score}" }
|
|
543
|
+
# break if cursor == 0
|
|
544
|
+
# end
|
|
545
|
+
#
|
|
546
|
+
# @example Scan with pattern matching
|
|
547
|
+
# cursor, members = zset.scan(0, match: 'user:*', count: 100)
|
|
548
|
+
#
|
|
549
|
+
def scan(cursor = 0, match: nil, count: nil)
|
|
550
|
+
opts = {}
|
|
551
|
+
opts[:match] = match if match
|
|
552
|
+
opts[:count] = count if count
|
|
553
|
+
|
|
554
|
+
new_cursor, result = dbclient.zscan(dbkey, cursor, **opts)
|
|
555
|
+
|
|
556
|
+
members = result.map { |member, score| [deserialize_value(member), score.to_f] }
|
|
557
|
+
[new_cursor.to_i, members]
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Stores the union of sorted sets into a destination key.
|
|
561
|
+
#
|
|
562
|
+
# @param destination [String] Destination key name
|
|
563
|
+
# @param other_sets [Array<SortedSet, String>] Other sorted sets or key names
|
|
564
|
+
# @param weights [Array<Numeric>, nil] Multiplication factors for each set's scores
|
|
565
|
+
# @param aggregate [Symbol, nil] How to aggregate scores (:sum, :min, :max)
|
|
566
|
+
# @return [Integer] Number of elements in the resulting sorted set
|
|
567
|
+
#
|
|
568
|
+
def unionstore(destination, *other_sets, weights: nil, aggregate: nil)
|
|
569
|
+
keys = [dbkey] + resolve_set_keys(other_sets)
|
|
570
|
+
opts = build_set_operation_opts(weights: weights, aggregate: aggregate)
|
|
571
|
+
|
|
572
|
+
dbclient.zunionstore(destination, keys, **opts)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Stores the intersection of sorted sets into a destination key.
|
|
576
|
+
#
|
|
577
|
+
# @param destination [String] Destination key name
|
|
578
|
+
# @param other_sets [Array<SortedSet, String>] Other sorted sets or key names
|
|
579
|
+
# @param weights [Array<Numeric>, nil] Multiplication factors for each set's scores
|
|
580
|
+
# @param aggregate [Symbol, nil] How to aggregate scores (:sum, :min, :max)
|
|
581
|
+
# @return [Integer] Number of elements in the resulting sorted set
|
|
582
|
+
#
|
|
583
|
+
def interstore(destination, *other_sets, weights: nil, aggregate: nil)
|
|
584
|
+
keys = [dbkey] + resolve_set_keys(other_sets)
|
|
585
|
+
opts = build_set_operation_opts(weights: weights, aggregate: aggregate)
|
|
586
|
+
|
|
587
|
+
dbclient.zinterstore(destination, keys, **opts)
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# Returns the difference between this sorted set and other sorted sets.
|
|
591
|
+
#
|
|
592
|
+
# @param other_sets [Array<SortedSet, String>] Other sorted sets or key names
|
|
593
|
+
# @param withscores [Boolean] Whether to include scores in result
|
|
594
|
+
# @return [Array] Members in this set but not in other sets
|
|
595
|
+
#
|
|
596
|
+
# @example Difference of two sorted sets
|
|
597
|
+
# zset.diff(other_zset) #=> ["unique_member"]
|
|
598
|
+
#
|
|
599
|
+
def diff(*other_sets, withscores: false)
|
|
600
|
+
keys = [dbkey] + resolve_set_keys(other_sets)
|
|
601
|
+
|
|
602
|
+
result = if withscores
|
|
603
|
+
dbclient.zdiff(*keys, withscores: true)
|
|
604
|
+
else
|
|
605
|
+
dbclient.zdiff(*keys)
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
process_set_operation_result(result, withscores: withscores)
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# Stores the difference of sorted sets into a destination key.
|
|
612
|
+
#
|
|
613
|
+
# @param destination [String] Destination key name
|
|
614
|
+
# @param other_sets [Array<SortedSet, String>] Other sorted sets or key names
|
|
615
|
+
# @return [Integer] Number of elements in the resulting sorted set
|
|
616
|
+
#
|
|
617
|
+
def diffstore(destination, *other_sets)
|
|
618
|
+
keys = [dbkey] + resolve_set_keys(other_sets)
|
|
619
|
+
dbclient.zdiffstore(destination, keys)
|
|
620
|
+
end
|
|
621
|
+
|
|
306
622
|
|
|
307
623
|
private
|
|
308
624
|
|
|
625
|
+
# Resolves sorted set arguments to their Redis key names.
|
|
626
|
+
#
|
|
627
|
+
# @param sets [Array<SortedSet, String>] Sorted sets or key names
|
|
628
|
+
# @return [Array<String>] Array of Redis key names
|
|
629
|
+
#
|
|
630
|
+
def resolve_set_keys(sets)
|
|
631
|
+
sets.map do |s|
|
|
632
|
+
case s
|
|
633
|
+
when Familia::SortedSet
|
|
634
|
+
s.dbkey
|
|
635
|
+
when String
|
|
636
|
+
s
|
|
637
|
+
else
|
|
638
|
+
raise ArgumentError, "Expected SortedSet or String key, got #{s.class}"
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# Builds options hash for set operations (union, inter, diff).
|
|
644
|
+
#
|
|
645
|
+
# @param weights [Array<Numeric>, nil] Score multiplication factors
|
|
646
|
+
# @param aggregate [Symbol, nil] Score aggregation method
|
|
647
|
+
# @param withscores [Boolean] Whether to include scores
|
|
648
|
+
# @return [Hash] Options hash for Redis command
|
|
649
|
+
#
|
|
650
|
+
def build_set_operation_opts(weights: nil, aggregate: nil, withscores: false)
|
|
651
|
+
opts = {}
|
|
652
|
+
opts[:weights] = weights if weights
|
|
653
|
+
opts[:aggregate] = aggregate.to_s.upcase if aggregate
|
|
654
|
+
opts[:withscores] = true if withscores
|
|
655
|
+
opts
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# Processes the result of set operations, deserializing values.
|
|
659
|
+
#
|
|
660
|
+
# @param result [Array] Raw result from Redis
|
|
661
|
+
# @param withscores [Boolean] Whether result includes scores
|
|
662
|
+
# @return [Array] Deserialized result
|
|
663
|
+
#
|
|
664
|
+
def process_set_operation_result(result, withscores: false)
|
|
665
|
+
return [] if result.nil? || result.empty?
|
|
666
|
+
|
|
667
|
+
if withscores
|
|
668
|
+
result.map { |member, score| [deserialize_value(member), score.to_f] }
|
|
669
|
+
else
|
|
670
|
+
deserialize_values(*result)
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
|
|
309
674
|
# Validates that mutually exclusive ZADD options are not specified together.
|
|
310
675
|
#
|
|
311
676
|
# @param nx [Boolean] NX option flag
|
|
@@ -133,11 +133,150 @@ module Familia
|
|
|
133
133
|
ret
|
|
134
134
|
end
|
|
135
135
|
|
|
136
|
+
# Atomically get and delete the value
|
|
137
|
+
# @return [String, nil] The value before deletion, or nil if key didn't exist
|
|
138
|
+
def getdel
|
|
139
|
+
dbclient.getdel(dbkey)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get value and optionally set expiration atomically
|
|
143
|
+
# @param ex [Integer, nil] Set expiration in seconds
|
|
144
|
+
# @param px [Integer, nil] Set expiration in milliseconds
|
|
145
|
+
# @param exat [Integer, nil] Set expiration at Unix timestamp (seconds)
|
|
146
|
+
# @param pxat [Integer, nil] Set expiration at Unix timestamp (milliseconds)
|
|
147
|
+
# @param persist [Boolean] Remove existing expiration
|
|
148
|
+
# @return [String, nil] The value
|
|
149
|
+
def getex(ex: nil, px: nil, exat: nil, pxat: nil, persist: false)
|
|
150
|
+
options = {}
|
|
151
|
+
options[:ex] = ex if ex
|
|
152
|
+
options[:px] = px if px
|
|
153
|
+
options[:exat] = exat if exat
|
|
154
|
+
options[:pxat] = pxat if pxat
|
|
155
|
+
options[:persist] = persist if persist
|
|
156
|
+
|
|
157
|
+
dbclient.getex(dbkey, **options)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Increment value by a float amount
|
|
161
|
+
# @param val [Float, Numeric] The amount to increment by
|
|
162
|
+
# @return [Float] The new value after increment
|
|
163
|
+
def incrbyfloat(val)
|
|
164
|
+
ret = dbclient.incrbyfloat(dbkey, val.to_f)
|
|
165
|
+
update_expiration
|
|
166
|
+
ret
|
|
167
|
+
end
|
|
168
|
+
alias incrfloat incrbyfloat
|
|
169
|
+
|
|
170
|
+
# Set value with expiration in seconds
|
|
171
|
+
# @param seconds [Integer] Expiration time in seconds
|
|
172
|
+
# @param val [Object] The value to set
|
|
173
|
+
# @return [String] "OK" on success
|
|
174
|
+
def setex(seconds, val)
|
|
175
|
+
dbclient.setex(dbkey, seconds.to_i, serialize_value(val))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Set value with expiration in milliseconds
|
|
179
|
+
# @param milliseconds [Integer] Expiration time in milliseconds
|
|
180
|
+
# @param val [Object] The value to set
|
|
181
|
+
# @return [String] "OK" on success
|
|
182
|
+
def psetex(milliseconds, val)
|
|
183
|
+
dbclient.psetex(dbkey, milliseconds.to_i, serialize_value(val))
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Count the number of set bits (population counting)
|
|
187
|
+
# @param start_pos [Integer, nil] Start byte position (optional)
|
|
188
|
+
# @param end_pos [Integer, nil] End byte position (optional)
|
|
189
|
+
# @return [Integer] Number of bits set to 1
|
|
190
|
+
def bitcount(start_pos = nil, end_pos = nil)
|
|
191
|
+
if start_pos && end_pos
|
|
192
|
+
dbclient.bitcount(dbkey, start_pos, end_pos)
|
|
193
|
+
else
|
|
194
|
+
dbclient.bitcount(dbkey)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Find the position of the first bit set to 0 or 1
|
|
199
|
+
# @param bit [Integer] The bit value to search for (0 or 1)
|
|
200
|
+
# @param start_pos [Integer, nil] Start byte position (optional)
|
|
201
|
+
# @param end_pos [Integer, nil] End byte position (optional)
|
|
202
|
+
# @return [Integer] Position of the first bit, or -1 if not found
|
|
203
|
+
def bitpos(bit, start_pos = nil, end_pos = nil)
|
|
204
|
+
if start_pos && end_pos
|
|
205
|
+
dbclient.bitpos(dbkey, bit, start_pos, end_pos)
|
|
206
|
+
elsif start_pos
|
|
207
|
+
dbclient.bitpos(dbkey, bit, start_pos)
|
|
208
|
+
else
|
|
209
|
+
dbclient.bitpos(dbkey, bit)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Perform bitfield operations on this string
|
|
214
|
+
# @param args [Array] Bitfield subcommands and arguments
|
|
215
|
+
# @return [Array] Results of the bitfield operations
|
|
216
|
+
# @example Get an unsigned 8-bit integer at offset 0
|
|
217
|
+
# str.bitfield('GET', 'u8', 0)
|
|
218
|
+
# @example Set and increment
|
|
219
|
+
# str.bitfield('SET', 'u8', 0, 100, 'INCRBY', 'i5', 100, 1)
|
|
220
|
+
def bitfield(*args)
|
|
221
|
+
ret = dbclient.bitfield(dbkey, *args)
|
|
222
|
+
update_expiration
|
|
223
|
+
ret
|
|
224
|
+
end
|
|
225
|
+
|
|
136
226
|
def del
|
|
137
227
|
ret = dbclient.del dbkey
|
|
138
228
|
ret.positive?
|
|
139
229
|
end
|
|
140
230
|
|
|
231
|
+
# Class methods for multi-key operations
|
|
232
|
+
class << self
|
|
233
|
+
# Get values for multiple keys
|
|
234
|
+
# @param keys [Array<String>] Full Redis key names
|
|
235
|
+
# @param client [Redis, nil] Optional Redis client (uses Familia.dbclient if nil)
|
|
236
|
+
# @return [Array] Values for each key (nil for non-existent keys)
|
|
237
|
+
# @example
|
|
238
|
+
# StringKey.mget('user:1:name', 'user:2:name')
|
|
239
|
+
def mget(*keys, client: nil)
|
|
240
|
+
client ||= Familia.dbclient
|
|
241
|
+
client.mget(*keys)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Set multiple keys atomically
|
|
245
|
+
# @param hash [Hash] Key-value pairs to set
|
|
246
|
+
# @param client [Redis, nil] Optional Redis client (uses Familia.dbclient if nil)
|
|
247
|
+
# @return [String] "OK" on success
|
|
248
|
+
# @example
|
|
249
|
+
# StringKey.mset('user:1:name' => 'Alice', 'user:2:name' => 'Bob')
|
|
250
|
+
def mset(hash, client: nil)
|
|
251
|
+
client ||= Familia.dbclient
|
|
252
|
+
client.mset(*hash.flatten)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Set multiple keys only if none of them exist
|
|
256
|
+
# @param hash [Hash] Key-value pairs to set
|
|
257
|
+
# @param client [Redis, nil] Optional Redis client (uses Familia.dbclient if nil)
|
|
258
|
+
# @return [Boolean] true if all keys were set, false if none were set
|
|
259
|
+
# @example
|
|
260
|
+
# StringKey.msetnx('user:1:name' => 'Alice', 'user:2:name' => 'Bob')
|
|
261
|
+
def msetnx(hash, client: nil)
|
|
262
|
+
client ||= Familia.dbclient
|
|
263
|
+
client.msetnx(*hash.flatten)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Perform bitwise operations between strings and store result
|
|
267
|
+
# @param operation [String, Symbol] Bitwise operation: AND, OR, XOR, NOT
|
|
268
|
+
# @param destkey [String] Destination key to store result
|
|
269
|
+
# @param keys [Array<String>] Source keys for the operation
|
|
270
|
+
# @param client [Redis, nil] Optional Redis client (uses Familia.dbclient if nil)
|
|
271
|
+
# @return [Integer] Size of the resulting string in bytes
|
|
272
|
+
# @example
|
|
273
|
+
# StringKey.bitop(:and, 'result', 'key1', 'key2')
|
|
274
|
+
def bitop(operation, destkey, *keys, client: nil)
|
|
275
|
+
client ||= Familia.dbclient
|
|
276
|
+
client.bitop(operation.to_s.upcase, destkey, *keys)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
141
280
|
Familia::DataType.register self, :string
|
|
142
281
|
Familia::DataType.register self, :stringkey
|
|
143
282
|
end
|