Class: Familia::SortedSet

Inherits:
DataType show all
Defined in:
lib/familia/data_type/types/sorted_set.rb

Instance Attribute Summary collapse

Attributes included from Settings

#current_key_version, #default_expiration, #delim, #encryption_keys, #encryption_personalization, #logical_database, #prefix, #schema_path, #schema_validator, #schemas, #strict_write_order, #suffix, #transaction_mode

Instance Method Summary collapse

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

Methods included from DataType::DatabaseCommands

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

Methods included from DataType::Connection

#dbclient, #dbkey, #direct_access, #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, #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.

Instance Method Details

#<<(val) ⇒ Integer

Note:

This is a non-standard operation for sorted sets as it doesn't allow specifying a custom score. Use add or []= for more control.

Adds a new element to the sorted set with the current timestamp as the score.

This method provides a convenient way to add elements to the sorted set without explicitly specifying a score. It uses the current Unix timestamp as the score, which effectively sorts elements by their insertion time.

Examples:

sorted_set << "new_element"

Parameters:

  • val (Object)

    The value to be added to the sorted set.

Returns:

  • (Integer)

    Returns 1 if the element is new and added, 0 if the element already existed and the score was updated.



36
37
38
# File 'lib/familia/data_type/types/sorted_set.rb', line 36

def <<(val)
  add(val)
end

#[]=(val, score) ⇒ Object

NOTE: The argument order is the reverse of #add. We do this to more naturally align with how the [] and []= methods are used.

e.g. obj.metrics[VALUE] = SCORE obj.metrics[VALUE] # => SCORE



47
48
49
# File 'lib/familia/data_type/types/sorted_set.rb', line 47

def []=(val, score)
  add val, score
end

#add(val, score = nil, nx: false, xx: false, gt: false, lt: false, ch: false) ⇒ Boolean Also known as: add_element

Note:

GT and LT options do NOT prevent adding new elements, they only affect update behavior for existing elements.

Note:

Default behavior (no options) adds new elements and updates existing ones unconditionally, matching standard Redis ZADD semantics.

Note:

INCR option is not supported. Use the increment method for ZINCRBY operations.

Note:

This method executes a Redis ZADD 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.

Adds an element to the sorted set with an optional score and ZADD options.

This method supports Redis ZADD options for conditional adds and updates:

  • NX: Only add new elements (don't update existing)
  • XX: Only update existing elements (don't add new)
  • GT: Only update if new score > current score
  • LT: Only update if new score < current score
  • CH: Return changed count (new + updated) instead of just new count

Examples:

Add new element with timestamp

metrics.add('pageview', Familia.now.to_f)  #=> true

Preserve original timestamp on subsequent saves

index.add(email, Familia.now.to_f, nx: true)  #=> true
index.add(email, Familia.now.to_f, nx: true)  #=> false (unchanged)

Update timestamp only for existing entries

index.add(email, Familia.now.to_f, xx: true)  #=> false (if doesn't exist)

Only update if new score is higher (leaderboard)

scores.add(player, 1000, gt: true)  #=> true (new entry)
scores.add(player, 1500, gt: true)  #=> false (updated)
scores.add(player, 1200, gt: true)  #=> false (not updated, score lower)

Track total changes for analytics

changed = metrics.add(user, score, ch: true)  #=> true (new or updated)

Combined options: only update existing, only if score increases

index.add(key, new_score, xx: true, gt: true)

Parameters:

  • val (Object)

    The value to add to the sorted set

  • score (Numeric, nil) (defaults to: nil)

    The score for ranking (defaults to current timestamp)

  • nx (Boolean) (defaults to: false)

    Only add new elements, don't update existing (default: false)

  • xx (Boolean) (defaults to: false)

    Only update existing elements, don't add new (default: false)

  • gt (Boolean) (defaults to: false)

    Only update if new score > current score (default: false)

  • lt (Boolean) (defaults to: false)

    Only update if new score < current score (default: false)

  • ch (Boolean) (defaults to: false)

    Return changed count instead of added count (default: false)

Returns:

  • (Boolean)

    Returns the return value from the redis gem's ZADD command. Returns true if element was added or changed (with CH option), false if element score was updated without change tracking or no operation occurred due to option constraints (NX, XX, GT, LT).

Raises:

  • (ArgumentError)

    If mutually exclusive options are specified together (NX+XX, GT+LT, NX+GT, NX+LT)



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/familia/data_type/types/sorted_set.rb', line 109

def add(val, score = nil, nx: false, xx: false, gt: false, lt: false, ch: false)
  warn_if_dirty!
  score ||= Familia.now

  # Validate mutual exclusivity
  validate_zadd_options!(nx: nx, xx: xx, gt: gt, lt: lt)

  # Build options hash for redis gem
  opts = {}
  opts[:nx] = true if nx
  opts[:xx] = true if xx
  opts[:gt] = true if gt
  opts[:lt] = true if lt
  opts[:ch] = true if ch

  # Pass options to ZADD
  ret = if opts.empty?
    dbclient.zadd(dbkey, score, serialize_value(val))
  else
    dbclient.zadd(dbkey, score, serialize_value(val), **opts)
  end

  update_expiration
  ret
end

#at(idx) ⇒ Object



308
309
310
# File 'lib/familia/data_type/types/sorted_set.rb', line 308

def at(idx)
  range(idx, idx).first
end

#collectObject



192
193
194
# File 'lib/familia/data_type/types/sorted_set.rb', line 192

def collect(&)
  members.collect(&)
end

#collectrawObject



208
209
210
# File 'lib/familia/data_type/types/sorted_set.rb', line 208

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

#decrement(val, by = 1) ⇒ Object Also known as: decr, decrby



290
291
292
# File 'lib/familia/data_type/types/sorted_set.rb', line 290

def decrement(val, by = 1)
  increment val, -by
end

#eachObject



184
185
186
# File 'lib/familia/data_type/types/sorted_set.rb', line 184

def each(&)
  members.each(&)
end

#each_with_indexObject



188
189
190
# File 'lib/familia/data_type/types/sorted_set.rb', line 188

def each_with_index(&)
  members.each_with_index(&)
end

#eachrawObject



200
201
202
# File 'lib/familia/data_type/types/sorted_set.rb', line 200

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

#eachraw_with_indexObject



204
205
206
# File 'lib/familia/data_type/types/sorted_set.rb', line 204

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

#element_countInteger Also known as: size, length, count

Returns the number of elements in the sorted set

Returns:

  • (Integer)

    number of elements



9
10
11
# File 'lib/familia/data_type/types/sorted_set.rb', line 9

def element_count
  dbclient.zcard dbkey
end

#empty?Boolean

Returns:

  • (Boolean)


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

def empty?
  element_count.zero?
end

#firstObject

Return the first element in the list. Redis: ZRANGE(0)



313
314
315
# File 'lib/familia/data_type/types/sorted_set.rb', line 313

def first
  at(0)
end

#increment(val, by = 1) ⇒ Object Also known as: incr, incrby



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

def increment(val, by = 1)
  ret = dbclient.zincrby(dbkey, by, serialize_value(val)).to_f
  update_expiration
  ret
end

#lastObject

Return the last element in the list. Redis: ZRANGE(-1)



318
319
320
# File 'lib/familia/data_type/types/sorted_set.rb', line 318

def last
  at(-1)
end

#member?(val) ⇒ Boolean Also known as: include?

Returns:

  • (Boolean)


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

def member?(val)
  Familia.trace :MEMBER, nil, "#{val}<#{val.class}>" if Familia.debug?
  !rank(val).nil?
end

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



160
161
162
163
164
# File 'lib/familia/data_type/types/sorted_set.rb', line 160

def members(count = -1, opts = {})
  count -= 1 if count.positive?
  elements = membersraw count, opts
  deserialize_values(*elements)
end

#membersraw(count = -1,, opts = {}) ⇒ Object



168
169
170
171
# File 'lib/familia/data_type/types/sorted_set.rb', line 168

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

#range(sidx, eidx, opts = {}) ⇒ Object



216
217
218
219
220
# File 'lib/familia/data_type/types/sorted_set.rb', line 216

def range(sidx, eidx, opts = {})
  echo :range, Familia.pretty_stack(limit: 1) if Familia.debug
  elements = rangeraw(sidx, eidx, opts)
  deserialize_values(*elements)
end

#rangebyscore(sscore, escore, opts = {}) ⇒ Object

e.g. obj.metrics.rangebyscore (now-12.hours), now, :limit => [0, 10]



244
245
246
247
248
# File 'lib/familia/data_type/types/sorted_set.rb', line 244

def rangebyscore(sscore, escore, opts = {})
  echo :rangebyscore, Familia.pretty_stack(limit: 1) if Familia.debug
  elements = rangebyscoreraw(sscore, escore, opts)
  deserialize_values(*elements)
end

#rangebyscoreraw(sscore, escore, opts = {}) ⇒ Object



250
251
252
253
# File 'lib/familia/data_type/types/sorted_set.rb', line 250

def rangebyscoreraw(sscore, escore, opts = {})
  echo :rangebyscoreraw, Familia.pretty_stack(limit: 1) if Familia.debug
  dbclient.zrangebyscore(dbkey, sscore, escore, **opts)
end

#rangeraw(sidx, eidx, opts = {}) ⇒ Object



222
223
224
225
226
227
228
229
230
231
# File 'lib/familia/data_type/types/sorted_set.rb', line 222

def rangeraw(sidx, eidx, opts = {})
  # NOTE: :withscores (no underscore) is the correct naming for the
  # redis-4.x gem. We pass :withscores through explicitly b/c
  # dbclient.zrange et al only accept that one optional argument.
  # Passing `opts`` through leads to an ArgumentError:
  #
  #   sorted_sets.rb:374:in `zrevrange': wrong number of arguments (given 4, expected 3) (ArgumentError)
  #
  dbclient.zrange(dbkey, sidx, eidx, **opts)
end

#rank(v) ⇒ Object

rank of member +v+ when ordered lowest to highest (starts at 0)



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

def rank(v)
  ret = dbclient.zrank dbkey, serialize_value(v)
  ret&.to_i
end

#remove_element(value) ⇒ Integer Also known as: remove

Removes a member from the sorted set

Parameters:

  • value

    The value to remove from the sorted set

Returns:

  • (Integer)

    The number of members that were removed (0 or 1)



299
300
301
302
303
304
305
# File 'lib/familia/data_type/types/sorted_set.rb', line 299

def remove_element(value)
  warn_if_dirty!
  Familia.trace :REMOVE_ELEMENT, nil, "#{value}<#{value.class}>" if Familia.debug?
  ret = dbclient.zrem dbkey, serialize_value(value)
  update_expiration
  ret
end

#remrangebyrank(srank, erank) ⇒ Object



268
269
270
271
272
273
# File 'lib/familia/data_type/types/sorted_set.rb', line 268

def remrangebyrank(srank, erank)
  warn_if_dirty!
  ret = dbclient.zremrangebyrank dbkey, srank, erank
  update_expiration
  ret
end

#remrangebyscore(sscore, escore) ⇒ Object



275
276
277
278
279
280
# File 'lib/familia/data_type/types/sorted_set.rb', line 275

def remrangebyscore(sscore, escore)
  warn_if_dirty!
  ret = dbclient.zremrangebyscore dbkey, sscore, escore
  update_expiration
  ret
end

#revmembers(count = -1,, opts = {}) ⇒ Object



173
174
175
176
177
# File 'lib/familia/data_type/types/sorted_set.rb', line 173

def revmembers(count = -1, opts = {})
  count -= 1 if count.positive?
  elements = revmembersraw count, opts
  deserialize_values(*elements)
end

#revmembersraw(count = -1,, opts = {}) ⇒ Object



179
180
181
182
# File 'lib/familia/data_type/types/sorted_set.rb', line 179

def revmembersraw(count = -1, opts = {})
  count -= 1 if count.positive?
  revrangeraw 0, count, opts
end

#revrange(sidx, eidx, opts = {}) ⇒ Object



233
234
235
236
237
# File 'lib/familia/data_type/types/sorted_set.rb', line 233

def revrange(sidx, eidx, opts = {})
  echo :revrange, Familia.pretty_stack(limit: 1) if Familia.debug
  elements = revrangeraw(sidx, eidx, opts)
  deserialize_values(*elements)
end

#revrangebyscore(sscore, escore, opts = {}) ⇒ Object

e.g. obj.metrics.revrangebyscore (now-12.hours), now, :limit => [0, 10]



256
257
258
259
260
# File 'lib/familia/data_type/types/sorted_set.rb', line 256

def revrangebyscore(sscore, escore, opts = {})
  echo :revrangebyscore, Familia.pretty_stack(limit: 1) if Familia.debug
  elements = revrangebyscoreraw(sscore, escore, opts)
  deserialize_values(*elements)
end

#revrangebyscoreraw(sscore, escore, opts = {}) ⇒ Object



262
263
264
265
266
# File 'lib/familia/data_type/types/sorted_set.rb', line 262

def revrangebyscoreraw(sscore, escore, opts = {})
  echo :revrangebyscoreraw, Familia.pretty_stack(limit: 1) if Familia.debug
  opts[:with_scores] = true if opts[:withscores]
  dbclient.zrevrangebyscore(dbkey, sscore, escore, opts)
end

#revrangeraw(sidx, eidx, opts = {}) ⇒ Object



239
240
241
# File 'lib/familia/data_type/types/sorted_set.rb', line 239

def revrangeraw(sidx, eidx, opts = {})
  dbclient.zrevrange(dbkey, sidx, eidx, **opts)
end

#revrank(v) ⇒ Object

rank of member +v+ when ordered highest to lowest (starts at 0)



155
156
157
158
# File 'lib/familia/data_type/types/sorted_set.rb', line 155

def revrank(v)
  ret = dbclient.zrevrank dbkey, serialize_value(v)
  ret&.to_i
end

#score(val) ⇒ Object Also known as: []



136
137
138
139
# File 'lib/familia/data_type/types/sorted_set.rb', line 136

def score(val)
  ret = dbclient.zscore dbkey, serialize_value(val)
  ret&.to_f
end

#selectObject



196
197
198
# File 'lib/familia/data_type/types/sorted_set.rb', line 196

def select(&)
  members.select(&)
end

#selectrawObject



212
213
214
# File 'lib/familia/data_type/types/sorted_set.rb', line 212

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