Class: Familia::ListKey

Inherits:
DataType show all
Includes:
DataType::CollectionBase
Defined in:
lib/familia/data_type/types/listkey.rb

Instance Attribute Summary collapse

Attributes included from Settings

#current_key_version, #default_expiration, #delim, #encryption_hkdf_salt, #encryption_hkdf_salt_history, #encryption_keys, #encryption_personalization, #encryption_personalization_history, #logical_database, #prefix, #raise_on_unsaved_parent_write, #schema_path, #schema_validator, #schemas, #strict_write_order, #suffix, #transaction_mode

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DataType::CollectionBase

#collection_type?, #each_record

Methods included from Features::Autoloader

autoload_files, included, normalize_to_config_name

Methods included from DataType::Serialization

#deserialize_value, #deserialize_values, #deserialize_values_with_nil, #serialize_value, #strip_legacy_json_encoding

Methods included from DataType::DatabaseCommands

#current_expiration, #delete!, #echo, #exists?, #expire, #expireat, #persist, #rename, #renamenx, #type

Methods included from DataType::Connection

#dbclient, #dbkey, #uri

Methods included from Connection::Behavior

#connect, #create_dbclient, #multi, #normalize_uri, #pipeline, #pipelined, #transaction, #uri=, #url, #url=

Methods included from Settings

#configure, #default_suffix, #dirty_write_warnings, #dirty_write_warnings=, #pipelined_mode, #pipelined_mode=

Methods included from Base

add_feature, #as_json, #expired?, #expires?, find_feature, #generate_id, #to_json, #to_s, #ttl, #update_expiration, #uuid

Constructor Details

This class inherits a constructor from Familia::DataType

Instance Attribute Details

#features_enabledObject (readonly) Originally defined in module Features

Returns the value of attribute features_enabled.

#logical_database(val = nil) ⇒ Object Originally defined in module DataType::ClassMethods

#parentObject Originally defined in module DataType::ClassMethods

Returns the value of attribute parent.

#prefixObject Originally defined in module DataType::ClassMethods

Returns the value of attribute prefix.

#suffixObject Originally defined in module DataType::ClassMethods

Returns the value of attribute suffix.

#uri(val = nil) ⇒ Object Originally defined in module DataType::ClassMethods

Returns the value of attribute uri.

Class Method Details

.supports_max_length?Boolean

ListKey implements :max_length (capped collection, issue #351) with per-end semantics — intentionally not unified with SortedSet's score-based trim: #push trims from the head (keeps the newest tail, LTRIM -max_length -1), while #unshift trims from the tail (keeps the newest head, LTRIM 0 max_length-1). Either way the max_length most recently written elements survive. NOT capped: pushx/unshiftx, insert, set, move destinations, and external writers.

Returns:

  • (Boolean)


16
17
18
# File 'lib/familia/data_type/types/listkey.rb', line 16

def self.supports_max_length?
  true
end

Instance Method Details

#<<(val) ⇒ Object Also known as: add_element, add



58
59
60
# File 'lib/familia/data_type/types/listkey.rb', line 58

def <<(val)
  push(val)
end

#[](idx, count = nil) ⇒ Object Also known as: slice



118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/familia/data_type/types/listkey.rb', line 118

def [](idx, count = nil)
  if idx.is_a? Range
    range idx.first, idx.last
  elsif count
    case count <=> 0
    when 1  then range(idx, idx + count - 1)
    when 0  then []
    when -1 then nil
    end
  else
    at idx
  end
end

#at(idx) ⇒ Object



222
223
224
# File 'lib/familia/data_type/types/listkey.rb', line 222

def at(idx)
  deserialize_value dbclient.lindex(dbkey, idx)
end

#collectrawObject



214
215
216
# File 'lib/familia/data_type/types/listkey.rb', line 214

def collectraw(&)
  rangeraw.collect(&)
end

#each(batch_size: 100) {|element| ... } ⇒ Enumerator, self

Iterates over elements of the list.

Uses LRANGE pagination for memory-efficient iteration over large lists. Unlike sets, Redis lists do not support SCAN, so we paginate through the list using index ranges.

Examples:

Iterate all elements

history.each { |event| process(event) }

Use as Enumerator

history.each.with_index { |event, idx| puts "#{idx}: #{event}" }

Parameters:

  • batch_size (Integer) (defaults to: 100)

    Number of elements to fetch per LRANGE call

Yields:

  • (element)

    Each deserialized element

Returns:

  • (Enumerator, self)

    Returns Enumerator if no block given, self otherwise



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/familia/data_type/types/listkey.rb', line 187

def each(batch_size: 100, &block)
  return to_enum(:each, batch_size: batch_size) unless block

  offset = 0
  loop do
    # LRANGE is inclusive on both ends, so end_idx = offset + batch_size - 1
    elements = range(offset, offset + batch_size - 1)
    break if elements.empty?

    elements.each(&block)
    offset += elements.size

    # If we got fewer than batch_size, we've reached the end
    break if elements.size < batch_size
  end

  self
end

#eachrawObject



206
207
208
# File 'lib/familia/data_type/types/listkey.rb', line 206

def eachraw(&)
  rangeraw.each(&)
end

#eachraw_with_indexObject



210
211
212
# File 'lib/familia/data_type/types/listkey.rb', line 210

def eachraw_with_index(&)
  rangeraw.each_with_index(&)
end

#element_countInteger Also known as: size, length, count

Returns the number of elements in the list

Returns:

  • (Integer)

    number of elements



22
23
24
# File 'lib/familia/data_type/types/listkey.rb', line 22

def element_count
  dbclient.llen dbkey
end

#empty?Boolean

Returns:

  • (Boolean)


29
30
31
# File 'lib/familia/data_type/types/listkey.rb', line 29

def empty?
  element_count.zero?
end

#enforce_max_length!(keep: :tail) ⇒ Integer, Redis::Future

Trims an already-oversized list down to its :max_length cap. Capped writes enforce the cap going forward, but nothing runs at definition time: adding max_length: to a live collection leaves the existing overage in place until the next push/unshift. Call this once, e.g. as a migration step, to enforce the cap immediately.

The cap's per-end semantics belong to the write method (push keeps the tail, unshift keeps the head), so a manual trim must be told which end to keep: keep: :tail matches a push-fed list (the default), keep: :head an unshift-fed one.

LLEN and LTRIM run in one MULTI so the removed count is exact even under concurrent writes. Inside a caller transaction or pipeline the pair is queued bare and the pre-trim length's future is returned instead of a count.

Parameters:

  • keep (:tail, :head) (defaults to: :tail)

    which end of the list survives the trim

Returns:

  • (Integer, Redis::Future)

    number of elements removed (0 when already within cap); inside a caller transaction or pipeline, the future of the pre-trim length instead

Raises:



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/familia/data_type/types/listkey.rb', line 259

def enforce_max_length!(keep: :tail)
  max = require_max_length!
  start, stop = case keep
                when :tail then [-max, -1]
                when :head then [0, max - 1]
                else
                  raise ArgumentError, "keep must be :tail or :head, got #{keep.inspect}"
  end
  warn_if_dirty!
  length = execute_capped_write do |conn|
    ret = conn.llen(dbkey)
    conn.ltrim(dbkey, start, stop)
    ret
  end
  update_expiration
  length.is_a?(Integer) ? [length - max, 0].max : length
end

#firstObject



362
363
364
# File 'lib/familia/data_type/types/listkey.rb', line 362

def first
  at 0
end

#insert(position, pivot, value) ⇒ Integer Also known as: linsert

Inserts an element before or after a pivot element

Parameters:

  • position (:before, :after)

    Where to insert relative to pivot

  • pivot

    The pivot element to search for

  • value

    The value to insert

Returns:

  • (Integer)

    Length of list after insert, or -1 if pivot not found



295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/familia/data_type/types/listkey.rb', line 295

def insert(position, pivot, value)
  warn_if_dirty!
  pos = case position
        when :before, 'BEFORE' then 'BEFORE'
        when :after, 'AFTER' then 'AFTER'
        else
          raise ArgumentError, "position must be :before or :after, got #{position.inspect}"
  end
  result = dbclient.linsert dbkey, pos, serialize_value(pivot), serialize_value(value)
  update_expiration if Familia.positive?(result) == true
  result
end

#lastObject



366
367
368
# File 'lib/familia/data_type/types/listkey.rb', line 366

def last
  at(-1)
end

#member?(value) ⇒ Boolean

Returns:

  • (Boolean)


133
134
135
# File 'lib/familia/data_type/types/listkey.rb', line 133

def member?(value)
  !dbclient.lpos(dbkey, serialize_value(value)).nil?
end

#members(count = -1)) ⇒ Object Also known as: all, to_a



158
159
160
161
162
# File 'lib/familia/data_type/types/listkey.rb', line 158

def members(count = -1)
  echo :members, Familia.pretty_stack(limit: 1) if Familia.debug
  count -= 1 if count.positive?
  range 0, count
end

#membersraw(count = -1)) ⇒ Object



166
167
168
169
# File 'lib/familia/data_type/types/listkey.rb', line 166

def membersraw(count = -1)
  count -= 1 if count.positive?
  rangeraw 0, count
end

#move(destination, wherefrom, whereto) ⇒ Object? Also known as: lmove

Moves an element from this list to another list atomically

Parameters:

  • destination (ListKey, String)

    Destination list (ListKey or key string)

  • wherefrom (:left, :right)

    Which end to pop from source

  • whereto (:left, :right)

    Which end to push to destination

Returns:

  • (Object, nil)

    The moved element, or nil if source is empty



314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/familia/data_type/types/listkey.rb', line 314

def move(destination, wherefrom, whereto)
  warn_if_dirty!
  dest_key = destination.respond_to?(:dbkey) ? destination.dbkey : destination
  from = wherefrom.to_s.upcase
  to = whereto.to_s.upcase

  unless %w[LEFT RIGHT].include?(from) && %w[LEFT RIGHT].include?(to)
    raise ArgumentError, 'wherefrom and whereto must be :left or :right'
  end

  result = dbclient.lmove dbkey, dest_key, from, to
  update_expiration
  deserialize_value result
end

#pop(count = nil) ⇒ Object, ...

Removes and returns the last element(s) from the list

Parameters:

  • count (Integer, nil) (defaults to: nil)

    Number of elements to pop (Redis 6.2+)

Returns:

  • (Object, Array<Object>, nil)

    Single element or array if count specified



91
92
93
94
95
96
97
98
99
100
101
# File 'lib/familia/data_type/types/listkey.rb', line 91

def pop(count = nil)
  warn_if_dirty!
  ret = if count
    result = dbclient.rpop(dbkey, count)
    result.nil? ? nil : deserialize_values(*result)
  else
    deserialize_value dbclient.rpop(dbkey)
  end
  update_expiration
  ret
end

#push(*values) ⇒ Object Also known as: append

Note:

This method executes a Redis RPUSH immediately, unlike scalar field setters which are deferred until save. If the parent object has unsaved scalar field changes, consider calling save first to avoid split-brain state.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/familia/data_type/types/listkey.rb', line 36

def push(*values)
  warn_if_dirty!
  echo :push, Familia.pretty_stack(limit: 1) if Familia.debug
  serialized = values.flatten.compact.map { |v| serialize_value(v) }
  unless serialized.empty?
    if (max = @opts[:max_length])
      # Keep the tail: RPUSH appends there, so trimming from the head
      # retains the max_length newest elements.
      execute_capped_write do |conn|
        ret = conn.rpush(dbkey, serialized)
        conn.ltrim(dbkey, -max, -1)
        ret
      end
    else
      dbclient.rpush(dbkey, serialized)
    end
  end
  update_expiration
  self
end

#pushx(*values) ⇒ Integer Also known as: rpushx

Pushes values only if the list already exists

Parameters:

  • values

    Values to push to the tail of the list

Returns:

  • (Integer)

    Length of list after push, or 0 if list doesn't exist



333
334
335
336
337
338
339
340
341
342
343
# File 'lib/familia/data_type/types/listkey.rb', line 333

def pushx(*values)
  return 0 if values.empty?

  serialized_values = values.flatten.compact.map { |v| serialize_value(v) }
  return 0 if serialized_values.empty?

  warn_if_dirty!
  result = dbclient.rpushx(dbkey, serialized_values)
  update_expiration if Familia.positive?(result) == true
  result
end

#range(sidx = 0, eidx = -1)) ⇒ Object



149
150
151
152
# File 'lib/familia/data_type/types/listkey.rb', line 149

def range(sidx = 0, eidx = -1)
  elements = rangeraw sidx, eidx
  deserialize_values(*elements)
end

#rangeraw(sidx = 0, eidx = -1)) ⇒ Object



154
155
156
# File 'lib/familia/data_type/types/listkey.rb', line 154

def rangeraw(sidx = 0, eidx = -1)
  dbclient.lrange(dbkey, sidx, eidx)
end

#remove_element(value, count = 0) ⇒ Integer Also known as: remove

Removes elements equal to value from the list

Parameters:

  • value

    The value to remove

  • count (Integer) (defaults to: 0)

    Number of elements to remove (0 means all)

Returns:

  • (Integer)

    The number of removed elements



141
142
143
144
145
146
# File 'lib/familia/data_type/types/listkey.rb', line 141

def remove_element(value, count = 0)
  warn_if_dirty!
  ret = dbclient.lrem dbkey, count, serialize_value(value)
  update_expiration
  ret
end

#selectrawObject



218
219
220
# File 'lib/familia/data_type/types/listkey.rb', line 218

def selectraw(&)
  rangeraw.select(&)
end

#set(index, value) ⇒ String Also known as: lset

Sets the element at the specified index

Parameters:

  • index (Integer)

    Index to set (0-based, negative counts from end)

  • value

    The value to set

Returns:

  • (String)

    "OK" on success

Raises:

  • (Redis::CommandError)

    if index is out of range



282
283
284
285
286
287
# File 'lib/familia/data_type/types/listkey.rb', line 282

def set(index, value)
  warn_if_dirty!
  result = dbclient.lset dbkey, index, serialize_value(value)
  update_expiration
  result
end

#shift(count = nil) ⇒ Object, ...

Removes and returns the first element(s) from the list

Parameters:

  • count (Integer, nil) (defaults to: nil)

    Number of elements to shift (Redis 6.2+)

Returns:

  • (Object, Array<Object>, nil)

    Single element or array if count specified



106
107
108
109
110
111
112
113
114
115
116
# File 'lib/familia/data_type/types/listkey.rb', line 106

def shift(count = nil)
  warn_if_dirty!
  ret = if count
    result = dbclient.lpop(dbkey, count)
    result.nil? ? nil : deserialize_values(*result)
  else
    deserialize_value dbclient.lpop(dbkey)
  end
  update_expiration
  ret
end

#trim(start, stop) ⇒ String Also known as: ltrim

Trims the list to the specified range

Parameters:

  • start (Integer)

    Start index (0-based, negative counts from end)

  • stop (Integer)

    End index (inclusive, negative counts from end)

Returns:

  • (String)

    "OK" on success



230
231
232
233
234
235
# File 'lib/familia/data_type/types/listkey.rb', line 230

def trim(start, stop)
  warn_if_dirty!
  result = dbclient.ltrim dbkey, start, stop
  update_expiration
  result
end

#unshift(*values) ⇒ Object Also known as: prepend

Note:

This method executes a Redis LPUSH immediately, unlike scalar field setters which are deferred until save. If the parent object has unsaved scalar field changes, consider calling save first to avoid split-brain state.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/familia/data_type/types/listkey.rb', line 67

def unshift(*values)
  warn_if_dirty!
  serialized = values.flatten.compact.map { |v| serialize_value(v) }
  unless serialized.empty?
    if (max = @opts[:max_length])
      # Keep the head: LPUSH prepends there, so trimming from the tail
      # retains the max_length newest elements.
      execute_capped_write do |conn|
        ret = conn.lpush(dbkey, serialized)
        conn.ltrim(dbkey, 0, max - 1)
        ret
      end
    else
      dbclient.lpush(dbkey, serialized)
    end
  end
  update_expiration
  self
end

#unshiftx(*values) ⇒ Integer Also known as: lpushx

Pushes values to the head only if the list already exists

Parameters:

  • values

    Values to push to the head of the list

Returns:

  • (Integer)

    Length of list after push, or 0 if list doesn't exist



349
350
351
352
353
354
355
356
357
358
359
# File 'lib/familia/data_type/types/listkey.rb', line 349

def unshiftx(*values)
  return 0 if values.empty?

  serialized_values = values.flatten.compact.map { |v| serialize_value(v) }
  return 0 if serialized_values.empty?

  warn_if_dirty!
  result = dbclient.lpushx(dbkey, serialized_values)
  update_expiration if Familia.positive?(result) == true
  result
end