Class: Familia::HashKey

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

Instance Attribute Summary collapse

Attributes included from Settings

#current_key_version, #default_expiration, #delim, #encryption_keys, #encryption_personalization, #logical_database, #prefix, #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

#[](field) ⇒ Object Also known as: get



38
39
40
# File 'lib/familia/data_type/types/hashkey.rb', line 38

def [](field)
  deserialize_value dbclient.hget(dbkey, field.to_s)
end

#[]=(field, val) ⇒ Object Also known as: put, store, add

+return+ [Integer] Returns 1 if the field is new and added, 0 if the field already existed and the value was updated.



22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/familia/data_type/types/hashkey.rb', line 22

def []=(field, val)
  ret = dbclient.hset dbkey, field.to_s, serialize_value(val)
  update_expiration
  ret
rescue TypeError => e
  Familia.error "[hset]= #{e.message}"
  Familia.debug "[hset]= #{dbkey} #{field}=#{val}"
  echo :hset, Familia.pretty_stack(limit: 1) if Familia.debug # logs via echo to the db and back
  klass = val.class
  msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{dbkey}"
  raise e.class, msg
end

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



109
110
111
# File 'lib/familia/data_type/types/hashkey.rb', line 109

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

#empty?Boolean

Returns:

  • (Boolean)


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

def empty?
  field_count.zero?
end

#expire_fields(seconds, *fields) ⇒ Array<Integer> Also known as: hexpire

Note:

Requires Redis 7.4+

Sets expiration time in seconds on one or more hash fields.

Examples:

Set 1 hour TTL on specific fields

my_hash.expire_fields(3600, 'session_token', 'temp_data')

Parameters:

  • seconds (Integer)

    TTL in seconds

  • fields (Array<String>)

    One or more field names

Returns:

  • (Array<Integer>)

    Array of results for each field: -2 if field does not exist, 1 if expiration was set, 0 if expiration was not set (e.g., field has no expiration)



242
243
244
245
# File 'lib/familia/data_type/types/hashkey.rb', line 242

def expire_fields(seconds, *fields)
  string_fields = fields.flatten.compact.map(&:to_s)
  dbclient.call('HEXPIRE', dbkey, seconds, 'FIELDS', string_fields.size, *string_fields)
end

#expireat_fields(unix_time, *fields) ⇒ Array<Integer> Also known as: hexpireat

Note:

Requires Redis 7.4+

Sets absolute expiration time (Unix timestamp in seconds) on hash fields.

Examples:

Expire fields at midnight tonight

midnight = Time.now.to_i + (24 * 60 * 60)
my_hash.expireat_fields(midnight, 'daily_counter')

Parameters:

  • unix_time (Integer)

    Absolute Unix timestamp in seconds

  • fields (Array<String>)

    One or more field names

Returns:

  • (Array<Integer>)

    Array of results for each field



273
274
275
276
# File 'lib/familia/data_type/types/hashkey.rb', line 273

def expireat_fields(unix_time, *fields)
  string_fields = fields.flatten.compact.map(&:to_s)
  dbclient.call('HEXPIREAT', dbkey, unix_time, 'FIELDS', string_fields.size, *string_fields)
end

#expiretime_fields(*fields) ⇒ Array<Integer> Also known as: hexpiretime

Note:

Requires Redis 7.4+

Returns the absolute Unix expiration timestamp in seconds for hash fields.

Examples:

Get expiration timestamp

my_hash.expiretime_fields('session')  #=> [1700000000]

Parameters:

  • fields (Array<String>)

    One or more field names

Returns:

  • (Array<Integer>)

    Array of timestamps for each field: -2 if field does not exist, -1 if field has no expiration, otherwise the absolute Unix timestamp in seconds



350
351
352
353
# File 'lib/familia/data_type/types/hashkey.rb', line 350

def expiretime_fields(*fields)
  string_fields = fields.flatten.compact.map(&:to_s)
  dbclient.call('HEXPIRETIME', dbkey, 'FIELDS', string_fields.size, *string_fields)
end

#fetch(field, default = nil) ⇒ Object



43
44
45
46
47
48
49
50
51
52
# File 'lib/familia/data_type/types/hashkey.rb', line 43

def fetch(field, default = nil)
  ret = self[field.to_s]
  if ret.nil?
    raise IndexError, "No such index for: #{field}" if default.nil?

    default
  else
    ret
  end
end

#field_countInteger Also known as: size, length, count

Returns the number of fields in the hash

Returns:

  • (Integer)

    number of fields



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

def field_count
  dbclient.hlen dbkey
end

#hgetallObject Also known as: all



62
63
64
65
66
# File 'lib/familia/data_type/types/hashkey.rb', line 62

def hgetall
  dbclient.hgetall(dbkey).transform_values do |v|
    deserialize_value v
  end
end

#hsetnx(field, val) ⇒ Integer

Sets field in the hash stored at key to value, only if field does not yet exist. If field already exists, this operation has no effect.

Parameters:

  • field (String)

    The field name

  • val (Object)

    The value to set

Returns:

  • (Integer)

    1 if field is a new field and value was set, 0 if field already exists



74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/familia/data_type/types/hashkey.rb', line 74

def hsetnx(field, val)
  ret = dbclient.hsetnx dbkey, field.to_s, serialize_value(val)
  update_expiration if ret == 1
  ret
rescue TypeError => e
  Familia.error "[hsetnx] #{e.message}"
  Familia.debug "[hsetnx] #{dbkey} #{field}=#{val}"
  echo :hsetnx, Familia.pretty_stack(limit: 1) if Familia.debug # logs via echo to the db and back
  klass = val.class
  msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{dbkey}"
  raise e.class, msg
end

#incrbyfloat(field, by) ⇒ Float Also known as: incrfloat

Increments the float value of a hash field by the given amount.

Examples:

my_hash.incrbyfloat('temperature', 0.5)  #=> 23.5
my_hash.incrbyfloat('temperature', -1.2) #=> 22.3

Parameters:

  • field (String)

    The field name

  • by (Float, Integer)

    The amount to increment by (can be negative)

Returns:

  • (Float)

    The new value after incrementing



174
175
176
# File 'lib/familia/data_type/types/hashkey.rb', line 174

def incrbyfloat(field, by)
  dbclient.hincrbyfloat(dbkey, field.to_s, by).to_f
end

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



103
104
105
# File 'lib/familia/data_type/types/hashkey.rb', line 103

def increment(field, by = 1)
  dbclient.hincrby(dbkey, field.to_s, by).to_i
end

#key?(field) ⇒ Boolean Also known as: has_key?, include?, member?

Returns:

  • (Boolean)


87
88
89
# File 'lib/familia/data_type/types/hashkey.rb', line 87

def key?(field)
  dbclient.hexists dbkey, field.to_s
end

#keysObject



54
55
56
# File 'lib/familia/data_type/types/hashkey.rb', line 54

def keys
  dbclient.hkeys dbkey
end

#persist_fields(*fields) ⇒ Array<Integer> Also known as: hpersist

Note:

Requires Redis 7.4+

Removes expiration from one or more hash fields.

Examples:

Remove expiration from fields

my_hash.persist_fields('important_data')  #=> [1]

Parameters:

  • fields (Array<String>)

    One or more field names

Returns:

  • (Array<Integer>)

    Array of results for each field: -2 if field does not exist, -1 if field has no expiration, 1 if expiration was removed



334
335
336
337
# File 'lib/familia/data_type/types/hashkey.rb', line 334

def persist_fields(*fields)
  string_fields = fields.flatten.compact.map(&:to_s)
  dbclient.call('HPERSIST', dbkey, 'FIELDS', string_fields.size, *string_fields)
end

#pexpire_fields(milliseconds, *fields) ⇒ Array<Integer> Also known as: hpexpire

Note:

Requires Redis 7.4+

Sets expiration time in milliseconds on one or more hash fields.

Examples:

Set 500ms TTL on a field

my_hash.pexpire_fields(500, 'rate_limit_counter')

Parameters:

  • milliseconds (Integer)

    TTL in milliseconds

  • fields (Array<String>)

    One or more field names

Returns:

  • (Array<Integer>)

    Array of results for each field



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

def pexpire_fields(milliseconds, *fields)
  string_fields = fields.flatten.compact.map(&:to_s)
  dbclient.call('HPEXPIRE', dbkey, milliseconds, 'FIELDS', string_fields.size, *string_fields)
end

#pexpireat_fields(unix_time_ms, *fields) ⇒ Array<Integer> Also known as: hpexpireat

Note:

Requires Redis 7.4+

Sets absolute expiration time (Unix timestamp in milliseconds) on hash fields.

Examples:

Expire field at a precise millisecond

my_hash.pexpireat_fields(1700000000000, 'precise_data')

Parameters:

  • unix_time_ms (Integer)

    Absolute Unix timestamp in milliseconds

  • fields (Array<String>)

    One or more field names

Returns:

  • (Array<Integer>)

    Array of results for each field



288
289
290
291
# File 'lib/familia/data_type/types/hashkey.rb', line 288

def pexpireat_fields(unix_time_ms, *fields)
  string_fields = fields.flatten.compact.map(&:to_s)
  dbclient.call('HPEXPIREAT', dbkey, unix_time_ms, 'FIELDS', string_fields.size, *string_fields)
end

#pexpiretime_fields(*fields) ⇒ Array<Integer> Also known as: hpexpiretime

Note:

Requires Redis 7.4+

Returns the absolute Unix expiration timestamp in milliseconds for hash fields.

Examples:

Get precise expiration timestamp

my_hash.pexpiretime_fields('session')  #=> [1700000000000]

Parameters:

  • fields (Array<String>)

    One or more field names

Returns:

  • (Array<Integer>)

    Array of timestamps in milliseconds



364
365
366
367
# File 'lib/familia/data_type/types/hashkey.rb', line 364

def pexpiretime_fields(*fields)
  string_fields = fields.flatten.compact.map(&:to_s)
  dbclient.call('HPEXPIRETIME', dbkey, 'FIELDS', string_fields.size, *string_fields)
end

#pttl_fields(*fields) ⇒ Array<Integer> Also known as: hpttl

Note:

Requires Redis 7.4+

Returns the remaining TTL in milliseconds for one or more hash fields.

Examples:

Check remaining TTL in milliseconds

my_hash.pttl_fields('rate_limit')  #=> [450]

Parameters:

  • fields (Array<String>)

    One or more field names

Returns:

  • (Array<Integer>)

    Array of TTL values in milliseconds



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

def pttl_fields(*fields)
  string_fields = fields.flatten.compact.map(&:to_s)
  dbclient.call('HPTTL', dbkey, 'FIELDS', string_fields.size, *string_fields)
end

#randfield(count = nil, withvalues: false) ⇒ String, ... Also known as: hrandfield

Returns one or more random fields from the hash.

Examples:

Get a single random field

my_hash.randfield  #=> "some_field"

Get 3 distinct random fields

my_hash.randfield(3)  #=> ["field1", "field2", "field3"]

Get 2 random fields with values

my_hash.randfield(2, withvalues: true)  #=> [["field1", value1], ["field2", value2]]

Parameters:

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

    Number of fields to return. If nil, returns a single field. If positive, returns distinct fields. If negative, allows duplicates.

  • withvalues (Boolean) (defaults to: false)

    If true, returns fields with their values

Returns:

  • (String, Array<String>, Array<Array>)

    Depending on arguments:

    • No count: single field name (or nil if hash is empty)
    • With count: array of field names
    • With count and withvalues: array of [field, value] pairs


210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/familia/data_type/types/hashkey.rb', line 210

def randfield(count = nil, withvalues: false)
  if count.nil?
    dbclient.hrandfield(dbkey)
  elsif withvalues
    pairs = dbclient.hrandfield(dbkey, count, 'WITHVALUES')
    # pairs is array of [field, value, field, value, ...]
    # Convert to array of [field, deserialized_value] pairs
    pairs.each_slice(2).map { |field, val| [field, deserialize_value(val)] }
  else
    dbclient.hrandfield(dbkey, count)
  end
end

#refreshself

The friendly neighborhood refresh method!

This method is like refresh! but with better manners - it returns self so you can chain it with other methods. It's perfect for when you want to refresh your hash and immediately do something with it.

Examples:

Refresh and chain

my_hash.refresh.keys  # Refresh and get all keys
my_hash.refresh['field']  # Refresh and get a specific field

Returns:

  • (self)

    Returns the refreshed hash, ready for more adventures!

Raises:

See Also:



421
422
423
424
# File 'lib/familia/data_type/types/hashkey.rb', line 421

def refresh
  refresh!
  self
end

#refresh!void

Note:

This operation is atomic - it either succeeds completely or fails safely. Any unsaved changes to the hash will be overwritten.

This method returns an undefined value.

The Great Database Refresh-o-matic 3000 for HashKey!

This method performs a complete refresh of the hash's state from the database. It's like giving your hash a memory transfusion - out with the old state, in with the fresh data straight from Valkey/Redis!

Examples:

Basic usage

my_hash.refresh!  # ZAP! Fresh data loaded

With error handling

begin
  my_hash.refresh!
rescue Familia::KeyNotFoundError
  puts "Oops! Our hash seems to have vanished into the Database void!"
end

Raises:



394
395
396
397
398
399
400
401
402
403
# File 'lib/familia/data_type/types/hashkey.rb', line 394

def refresh!
  Familia.trace :REFRESH, nil, self.class.uri if Familia.debug?
  raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)

  fields = hgetall
  Familia.debug "[refresh!] #{self.class} #{dbkey} #{fields.keys}"

  # For HashKey, we update by merging the fresh data
  update(fields)
end

#remove_field(field) ⇒ Integer Also known as: remove, remove_element

Removes a field from the hash

Parameters:

  • field (String)

    The field to remove

Returns:

  • (Integer)

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



97
98
99
# File 'lib/familia/data_type/types/hashkey.rb', line 97

def remove_field(field)
  dbclient.hdel dbkey, field.to_s
end

#scan(cursor = 0, match: nil, count: nil) ⇒ Array<String, Hash> Also known as: hscan

Incrementally iterates over fields in the hash using cursor-based iteration. This is more memory-efficient than hgetall for large hashes.

Examples:

Basic iteration

cursor = 0
loop do
  cursor, results = my_hash.scan(cursor)
  results.each { |field, value| puts "#{field}: #{value}" }
  break if cursor == "0"
end

With pattern matching

cursor, results = my_hash.scan(0, match: "user:*", count: 100)

Parameters:

  • cursor (Integer) (defaults to: 0)

    The cursor position to start from (0 for initial call)

  • match (String, nil) (defaults to: nil)

    Optional glob-style pattern to filter field names

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

    Optional hint for number of elements to return per call

Returns:

  • (Array<String, Hash>)

    A two-element array: [new_cursor, => value, ...] When new_cursor is "0", iteration is complete.



151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/familia/data_type/types/hashkey.rb', line 151

def scan(cursor = 0, match: nil, count: nil)
  args = [dbkey, cursor]
  args += ['MATCH', match] if match
  args += ['COUNT', count] if count

  new_cursor, pairs = dbclient.hscan(*args)

  # pairs is an array of [field, value] pairs, convert to hash with deserialization
  result_hash = pairs.to_h.transform_values { |v| deserialize_value(v) }

  [new_cursor, result_hash]
end

#strlen(field) ⇒ Integer Also known as: hstrlen

Returns the string length of the value associated with field.

Examples:

my_hash['name'] = 'Alice'
my_hash.strlen('name')  #=> 7 (includes JSON quotes: "Alice")

Parameters:

  • field (String)

    The field name

Returns:

  • (Integer)

    The length of the value in bytes, or 0 if field does not exist



187
188
189
# File 'lib/familia/data_type/types/hashkey.rb', line 187

def strlen(field)
  dbclient.hstrlen(dbkey, field.to_s)
end

#ttl_fields(*fields) ⇒ Array<Integer> Also known as: httl

Note:

Requires Redis 7.4+

Returns the remaining TTL in seconds for one or more hash fields.

Examples:

Check remaining TTL on fields

my_hash.ttl_fields('session_token', 'temp_data')  #=> [3600, -1]

Parameters:

  • fields (Array<String>)

    One or more field names

Returns:

  • (Array<Integer>)

    Array of TTL values for each field: -2 if field does not exist, -1 if field has no expiration, otherwise the TTL in seconds



304
305
306
307
# File 'lib/familia/data_type/types/hashkey.rb', line 304

def ttl_fields(*fields)
  string_fields = fields.flatten.compact.map(&:to_s)
  dbclient.call('HTTL', dbkey, 'FIELDS', string_fields.size, *string_fields)
end

#update(hsh = {}) ⇒ Object Also known as: merge!

Raises:

  • (ArgumentError)


115
116
117
118
119
120
121
122
123
# File 'lib/familia/data_type/types/hashkey.rb', line 115

def update(hsh = {})
  raise ArgumentError, 'Argument to bulk_set must be a hash' unless hsh.is_a?(Hash)

  data = hsh.inject([]) { |ret, pair| ret << [pair[0], serialize_value(pair[1])] }.flatten

  ret = dbclient.hmset(dbkey, *data)
  update_expiration
  ret
end

#valuesObject



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

def values
  dbclient.hvals(dbkey).map { |v| deserialize_value v }
end

#values_at(*fields) ⇒ Object



126
127
128
129
130
# File 'lib/familia/data_type/types/hashkey.rb', line 126

def values_at *fields
  string_fields = fields.flatten.compact.map(&:to_s)
  elements = dbclient.hmget(dbkey, *string_fields)
  deserialize_values(*elements)
end