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.
@@ -41,12 +41,32 @@ module Familia
41
41
  end
42
42
  alias prepend unshift
43
43
 
44
- def pop
45
- deserialize_value dbclient.rpop(dbkey)
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
- def shift
49
- deserialize_value dbclient.lpop(dbkey)
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