Class: Familia::HashKey

Inherits:
DataType show all
Includes:
DataType::CollectionBase
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, #schema_path, #schema_validator, #schemas, #strict_write_order, #suffix, #transaction_mode

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

Methods included from DataType::DatabaseCommands

#current_expiration, #delete!, #echo, #exists?, #expire, #expireat, #move, #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, #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



45
46
47
# File 'lib/familia/data_type/types/hashkey.rb', line 45

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

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

Note:

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

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



28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/familia/data_type/types/hashkey.rb', line 28

def []=(field, val)
  warn_if_dirty!
  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



122
123
124
# File 'lib/familia/data_type/types/hashkey.rb', line 122

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

#each(matching: nil, batch_size: 100) {|field, value| ... } ⇒ Enumerator, self

Note:

Pattern matches field names only (plain strings). To filter on values, use Enumerable#select instead.

Iterates over field-value pairs in the hash.

Uses HSCAN for memory-efficient iteration. Optionally filters by field name pattern using Redis MATCH.

Examples:

Iterate all pairs

settings.each { |field, value| puts "#{field}: #{value}" }

Filter by field name pattern

settings.each(matching: "cache_*") { |f, v| puts "#{f}: #{v}" }

Parameters:

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

    Optional glob-style pattern to filter field names (e.g., "user:", "_count"). Pattern is passed to Redis HSCAN MATCH and matches against field names (plain strings, not JSON-encoded).

  • batch_size (Integer) (defaults to: 100)

    Number of elements to fetch per HSCAN iteration

Yields:

  • (field, value)

    Each field-value pair (values are deserialized)

Returns:

  • (Enumerator, self)

    Returns Enumerator if no block given, self otherwise



167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/familia/data_type/types/hashkey.rb', line 167

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

  cursor = 0
  loop do
    new_cursor, pairs = scan(cursor, match: matching, count: batch_size)
    pairs.each(&block)
    cursor = new_cursor
    break if cursor.zero?
  end

  self
end

#empty?Boolean

Returns:

  • (Boolean)


18
19
20
# File 'lib/familia/data_type/types/hashkey.rb', line 18

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)



293
294
295
# File 'lib/familia/data_type/types/hashkey.rb', line 293

def expire_fields(seconds, *fields)
  call_hash_field_command('HEXPIRE', seconds, fields: 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



322
323
324
# File 'lib/familia/data_type/types/hashkey.rb', line 322

def expireat_fields(unix_time, *fields)
  call_hash_field_command('HEXPIREAT', unix_time, fields: 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



394
395
396
# File 'lib/familia/data_type/types/hashkey.rb', line 394

def expiretime_fields(*fields)
  call_hash_field_command('HEXPIRETIME', fields: fields)
end

#fetch(field, default = nil) ⇒ Object



50
51
52
53
54
55
56
57
58
59
# File 'lib/familia/data_type/types/hashkey.rb', line 50

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



11
12
13
# File 'lib/familia/data_type/types/hashkey.rb', line 11

def field_count
  dbclient.hlen dbkey
end

#hgetallObject Also known as: all



69
70
71
72
73
# File 'lib/familia/data_type/types/hashkey.rb', line 69

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



81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/familia/data_type/types/hashkey.rb', line 81

def hsetnx(field, val)
  warn_if_dirty!
  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



223
224
225
226
227
# File 'lib/familia/data_type/types/hashkey.rb', line 223

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

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



114
115
116
117
118
# File 'lib/familia/data_type/types/hashkey.rb', line 114

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

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

Returns:

  • (Boolean)


95
96
97
# File 'lib/familia/data_type/types/hashkey.rb', line 95

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

#keysObject



61
62
63
# File 'lib/familia/data_type/types/hashkey.rb', line 61

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



379
380
381
# File 'lib/familia/data_type/types/hashkey.rb', line 379

def persist_fields(*fields)
  call_hash_field_command('HPERSIST', fields: 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



307
308
309
# File 'lib/familia/data_type/types/hashkey.rb', line 307

def pexpire_fields(milliseconds, *fields)
  call_hash_field_command('HPEXPIRE', milliseconds, fields: 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



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

def pexpireat_fields(unix_time_ms, *fields)
  call_hash_field_command('HPEXPIREAT', unix_time_ms, fields: 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



407
408
409
# File 'lib/familia/data_type/types/hashkey.rb', line 407

def pexpiretime_fields(*fields)
  call_hash_field_command('HPEXPIRETIME', fields: 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



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

def pttl_fields(*fields)
  call_hash_field_command('HPTTL', fields: 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


261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/familia/data_type/types/hashkey.rb', line 261

def randfield(count = nil, withvalues: false)
  if count.nil?
    dbclient.hrandfield(dbkey)
  elsif withvalues
    pairs = dbclient.hrandfield(dbkey, count, withvalues: true)
    # 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:



479
480
481
482
# File 'lib/familia/data_type/types/hashkey.rb', line 479

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:



452
453
454
455
456
457
458
459
460
461
# File 'lib/familia/data_type/types/hashkey.rb', line 452

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)



105
106
107
108
109
110
# File 'lib/familia/data_type/types/hashkey.rb', line 105

def remove_field(field)
  warn_if_dirty!
  ret = dbclient.hdel dbkey, field.to_s
  update_expiration
  ret
end

#scan(cursor = 0, match: nil, count: nil) ⇒ Array<Integer, 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<Integer, Hash>)

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



200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/familia/data_type/types/hashkey.rb', line 200

def scan(cursor = 0, match: nil, count: nil)
  opts = {}
  opts[:match] = match if match
  opts[:count] = count if count

  new_cursor, pairs = dbclient.hscan(dbkey, cursor, **opts)

  # 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.to_i, 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



238
239
240
# File 'lib/familia/data_type/types/hashkey.rb', line 238

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



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

def ttl_fields(*fields)
  call_hash_field_command('HTTL', fields: fields)
end

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

Raises:

  • (ArgumentError)


128
129
130
131
132
133
134
135
136
137
# File 'lib/familia/data_type/types/hashkey.rb', line 128

def update(hsh = {})
  warn_if_dirty!
  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



65
66
67
# File 'lib/familia/data_type/types/hashkey.rb', line 65

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

#values_at(*fields) ⇒ Object



140
141
142
143
144
# File 'lib/familia/data_type/types/hashkey.rb', line 140

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