familia 1.0.0.pre.rc6 → 1.1.0.pre.rc1

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -1
  3. data/Gemfile.lock +3 -1
  4. data/README.md +5 -5
  5. data/VERSION.yml +2 -2
  6. data/familia.gemspec +1 -2
  7. data/lib/familia/base.rb +11 -11
  8. data/lib/familia/connection.rb +18 -2
  9. data/lib/familia/errors.rb +14 -0
  10. data/lib/familia/features/expiration.rb +13 -2
  11. data/lib/familia/features/quantization.rb +1 -1
  12. data/lib/familia/horreum/class_methods.rb +99 -41
  13. data/lib/familia/horreum/commands.rb +53 -14
  14. data/lib/familia/horreum/relations_management.rb +9 -2
  15. data/lib/familia/horreum/serialization.rb +32 -23
  16. data/lib/familia/horreum.rb +8 -2
  17. data/lib/familia/redistype/commands.rb +5 -2
  18. data/lib/familia/redistype/serialization.rb +17 -16
  19. data/lib/familia/redistype/types/hashkey.rb +166 -0
  20. data/lib/familia/{types → redistype/types}/list.rb +19 -14
  21. data/lib/familia/{types → redistype/types}/sorted_set.rb +23 -19
  22. data/lib/familia/{types → redistype/types}/string.rb +8 -6
  23. data/lib/familia/{types → redistype/types}/unsorted_set.rb +16 -12
  24. data/lib/familia/redistype.rb +19 -9
  25. data/lib/familia.rb +5 -1
  26. data/try/10_familia_try.rb +1 -1
  27. data/try/20_redis_type_try.rb +1 -1
  28. data/try/21_redis_type_zset_try.rb +1 -1
  29. data/try/22_redis_type_set_try.rb +1 -1
  30. data/try/23_redis_type_list_try.rb +2 -2
  31. data/try/24_redis_type_string_try.rb +3 -3
  32. data/try/25_redis_type_hash_try.rb +1 -1
  33. data/try/26_redis_bool_try.rb +2 -2
  34. data/try/27_redis_horreum_try.rb +4 -4
  35. data/try/30_familia_object_try.rb +8 -5
  36. data/try/40_customer_try.rb +6 -6
  37. metadata +15 -15
  38. data/lib/familia/types/hashkey.rb +0 -108
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d368f329560a1afd7252a2c8de830a7230334933cd00715b4b952e44d9fbf63
4
- data.tar.gz: edd7b843ff07cf87aa2fe46766d7b0fd6dfa814b5404c9ca04bc42de18b05619
3
+ metadata.gz: f7e5fe48e629eab30af342be4ad81f6afe5854b33b678b926b9c3470c0515627
4
+ data.tar.gz: 1d938baae35feb17f3d9855055152fdb0ba441317315fd3b2de6932e3c47e0cf
5
5
  SHA512:
6
- metadata.gz: 0f4db87dbaa2c12636d8293a477ef1e2a000364d3e6e2515cac90dad7277efc29d3670103cdf4f7697960ec90012ddec7071004146785ec7d3c838863e1a241b
7
- data.tar.gz: a1812fce749dc485753a10c39281feb1babab24fd95ecdd553398c4a07a800836d7bf4ac4a7574ee987d4d181842e089cd732efa343141a0bf4610594ea8f1cd
6
+ metadata.gz: 707e9aba376653fa439ab1ffc85c4b794f1f52444dbed86a093f1d9c93a1952d8f84fe465cdcf8798bf0f72e5a4099586f1895cad5225f6d81899deeedcbf3a5
7
+ data.tar.gz: a9323482330bef4a632d92fd6828da32f0be7aa4f107d659e4440a2e3a09ad3dd0c2011d90a5e1a3380ca85fdf5afe47fb4634e9cf21951986d92115523bfbba
data/Gemfile CHANGED
@@ -5,7 +5,9 @@ source 'https://rubygems.org'
5
5
  gemspec
6
6
 
7
7
  group :development, :test do
8
- gem 'pry-byebug', '~> 3.10.1', require: false
8
+ # byebug only works with MRI
9
+ gem 'byebug', '~> 11.0', require: false if RUBY_ENGINE == 'ruby'
10
+ gem 'pry-byebug', '~> 3.10.1', require: false if RUBY_ENGINE == 'ruby'
9
11
  gem 'rubocop', require: false
10
12
  gem 'rubocop-performance', require: false
11
13
  gem 'rubocop-thread_safety', require: false
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (1.0.0.pre.rc6)
4
+ familia (1.1.0.pre.rc1)
5
5
  redis (>= 4.8.1, < 6.0)
6
+ stringio (~> 3.1.1)
6
7
  uri-redis (~> 1.3)
7
8
 
8
9
  GEM
@@ -55,6 +56,7 @@ GEM
55
56
  rubocop (>= 0.90.0)
56
57
  ruby-progressbar (1.13.0)
57
58
  storable (0.10.0)
59
+ stringio (3.1.1)
58
60
  strscan (3.1.0)
59
61
  sysinfo (0.10.0)
60
62
  drydock (< 1.0)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Familia - 1.0.0-rc6 (August 2024)
1
+ # Familia - 1.1.0-rc1 (November 2024)
2
2
 
3
3
  **Organize and store Ruby objects in Redis. A powerful Ruby ORM (of sorts) for Redis.**
4
4
 
@@ -9,7 +9,7 @@ Familia provides a flexible and feature-rich way to interact with Redis using Ru
9
9
 
10
10
  Get it in one of the following ways:
11
11
 
12
- * In your Gemfile: `gem 'familia', '>= 1.0.0-rc4'`
12
+ * In your Gemfile: `gem 'familia', '>= 1.1.0-rc1'`
13
13
  * Install it by hand: `gem install familia --pre`
14
14
  * Or for development: `git clone git@github.com:delano/familia.git`
15
15
 
@@ -152,11 +152,11 @@ end
152
152
 
153
153
  ```ruby
154
154
  class ComplexObject < Familia::Horreum
155
- def to_redis
155
+ def serialize_value
156
156
  custom_serialization_method
157
157
  end
158
158
 
159
- def self.from_redis(data)
159
+ def self.deserialize_value(data)
160
160
  custom_deserialization_method(data)
161
161
  end
162
162
  end
@@ -188,7 +188,7 @@ flower.save
188
188
  ### Retrieving and Updating Objects
189
189
 
190
190
  ```ruby
191
- rose = Flower.from_identifier("rrose")
191
+ rose = Flower.find_by_id("rrose")
192
192
  rose.name = "Pink Rose"
193
193
  rose.save
194
194
  ```
data/VERSION.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
2
  :MAJOR: 1
3
- :MINOR: 0
3
+ :MINOR: 1
4
4
  :PATCH: 0
5
- :PRE: rc6
5
+ :PRE: rc1
data/familia.gemspec CHANGED
@@ -20,9 +20,8 @@ Gem::Specification.new do |spec|
20
20
  spec.required_ruby_version = Gem::Requirement.new('>= 2.7.8')
21
21
 
22
22
  spec.add_dependency 'redis', '>= 4.8.1', '< 6.0'
23
+ spec.add_dependency 'stringio', '~> 3.1.1'
23
24
  spec.add_dependency 'uri-redis', '~> 1.3'
24
25
 
25
- # byebug only works with MRI
26
- spec.add_development_dependency 'byebug', '~> 11.0' if RUBY_ENGINE == 'ruby'
27
26
  spec.metadata['rubygems_mfa_required'] = 'true'
28
27
  end
data/lib/familia/base.rb CHANGED
@@ -30,21 +30,21 @@ module Familia
30
30
  end
31
31
  end
32
32
 
33
- # Yo, this class is like that one friend who never checks expiration dates.
34
- # It's living life on the edge, data-style!
33
+ # Base implementation of update_expiration that maintains API compatibility
34
+ # with the :expiration feature's implementation.
35
35
  #
36
- # @param ttl [Integer, nil] Time To Live? More like Time To Laugh! This param
37
- # is here for compatibility, but it's as useful as a chocolate teapot.
36
+ # This is a no-op implementation that gets overridden by features like
37
+ # :expiration. It accepts an optional ttl parameter to maintain interface
38
+ # compatibility with the overriding implementations.
38
39
  #
39
- # @return [nil] Always returns nil. It's consistent in its laziness!
40
+ # @param ttl [Integer, nil] Time To Live in seconds (ignored in base implementation)
41
+ # @return [nil] Always returns nil
40
42
  #
41
- # @example Trying to teach an old dog new tricks
42
- # immortal_data.update_expiration(86400) # Nice try, but this data is here to stay!
43
+ # @note This is a no-op implementation. Classes that need expiration
44
+ # functionality should include the :expiration feature.
43
45
  #
44
- # @note This method is a no-op. It's like shouting into the void, but less echo-y.
45
- #
46
- def update_expiration(*)
47
- Familia.info "[update_expiration] Skipped for #{rediskey}. #{self.class} data is immortal!"
46
+ def update_expiration(ttl: nil)
47
+ Familia.ld "[update_expiration] Feature not enabled for #{self.class}. Key: #{rediskey} (caller: #{caller(1..1)})"
48
48
  nil
49
49
  end
50
50
 
@@ -6,6 +6,7 @@ require_relative '../../lib/redis_middleware'
6
6
  module Familia
7
7
  @uri = URI.parse 'redis://127.0.0.1'
8
8
  @redis_clients = {}
9
+ @redis_uri_by_class = {}
9
10
 
10
11
  # The Connection module provides Redis connection management for Familia.
11
12
  # It allows easy setup and access to Redis clients across different URIs.
@@ -25,17 +26,20 @@ module Familia
25
26
  # Establishes a connection to a Redis server.
26
27
  #
27
28
  # @param uri [String, URI, nil] The URI of the Redis server to connect to.
28
- # If nil, uses the default URI.
29
- # @return [Redis] The connected Redis client
29
+ # If nil, uses the default URI from `@redis_uri_by_class` or `Familia.uri`.
30
+ # @return [Redis] The connected Redis client.
31
+ # @raise [ArgumentError] If no URI is specified.
30
32
  # @example
31
33
  # Familia.connect('redis://localhost:6379')
32
34
  def connect(uri = nil)
33
35
  uri = URI.parse(uri) if uri.is_a?(String)
36
+ uri ||= @redis_uri_by_class[self]
34
37
  uri ||= Familia.uri
35
38
 
36
39
  raise ArgumentError, 'No URI specified' unless uri
37
40
 
38
41
  conf = uri.conf
42
+ @redis_uri_by_class[self] = uri.serverid
39
43
 
40
44
  if Familia.enable_redis_logging
41
45
  RedisLogger.logger = Familia.logger
@@ -49,6 +53,9 @@ module Familia
49
53
  end
50
54
 
51
55
  redis = Redis.new(conf)
56
+
57
+ # Close the existing connection if it exists
58
+ @redis_clients[uri.serverid].close if @redis_clients[uri.serverid]
52
59
  @redis_clients[uri.serverid] = redis
53
60
  end
54
61
 
@@ -72,6 +79,15 @@ module Familia
72
79
  @redis_clients[uri.serverid]
73
80
  end
74
81
 
82
+ # Retrieves the Redis client associated with the given class.
83
+ #
84
+ # @param klass [Class] The class for which to retrieve the Redis client.
85
+ # @return [Redis] The Redis client associated with the given class.
86
+ def redis_uri_by_class(klass)
87
+ uri = @redis_uri_by_class[klass]
88
+ connect(uri)
89
+ end
90
+
75
91
  # Sets the default URI for Redis connections.
76
92
  #
77
93
  # @param v [String, URI] The new default URI
@@ -30,4 +30,18 @@ module Familia
30
30
  "No client for #{uri.serverid}"
31
31
  end
32
32
  end
33
+
34
+ # Raised when attempting to refresh an object whose key doesn't exist in Redis
35
+ class KeyNotFoundError < Problem
36
+ attr_reader :key
37
+
38
+ def initialize(key)
39
+ @key = key
40
+ super
41
+ end
42
+
43
+ def message
44
+ "Key not found in Redis: #{key}"
45
+ end
46
+ end
33
47
  end
@@ -49,7 +49,7 @@ module Familia::Features
49
49
  # false otherwise.
50
50
  #
51
51
  # @example Setting an expiration of one day
52
- # object.update_expiration(86400)
52
+ # object.update_expiration(ttl: 86400)
53
53
  #
54
54
  # @note If TTL is set to zero, the expiration will be removed, making the
55
55
  # data persist indefinitely.
@@ -57,8 +57,19 @@ module Familia::Features
57
57
  # @raise [Familia::Problem] Raises an error if the TTL is not a non-negative
58
58
  # integer.
59
59
  #
60
- def update_expiration(ttl = nil)
60
+ def update_expiration(ttl: nil)
61
61
  ttl ||= self.ttl
62
+
63
+ if self.class.has_relations?
64
+ Familia.ld "[update_expiration] #{self.class} has relations: #{self.class.redis_types.keys}"
65
+ self.class.redis_types.each do |name, definition|
66
+ next if definition.opts[:ttl].nil?
67
+ obj = send(name)
68
+ Familia.ld "[update_expiration] Updating expiration for #{name} (#{obj.rediskey}) to #{ttl}"
69
+ obj.update_expiration(ttl: ttl)
70
+ end
71
+ end
72
+
62
73
  # It's important to raise exceptions here and not just log warnings. We
63
74
  # don't want to silently fail at setting expirations and cause data
64
75
  # retention issues (e.g. not removed in a timely fashion).
@@ -46,7 +46,7 @@ module Familia::Features
46
46
  end
47
47
 
48
48
  def qstamp(quantum = nil, pattern: nil, time: nil)
49
- self.class.qstamp(quantum || ttl, pattern: pattern, time: time)
49
+ self.class.qstamp(quantum || self.class.ttl, pattern: pattern, time: time)
50
50
  end
51
51
 
52
52
  extend ClassMethods
@@ -33,9 +33,6 @@ module Familia
33
33
  include Familia::Settings
34
34
  include Familia::Horreum::RelationsManagement
35
35
 
36
- attr_accessor :parent
37
- attr_writer :redis, :dump_method, :load_method
38
-
39
36
  # Returns the Redis connection for the class.
40
37
  #
41
38
  # This method retrieves the Redis connection instance for the class. If no
@@ -74,42 +71,90 @@ module Familia
74
71
  fields << name
75
72
  attr_accessor name
76
73
 
77
- # Every field gets a fast writer method for immediately persisting
78
- fast_writer! name
74
+ # Every field gets a fast attribute method for immediately persisting
75
+ fast_attribute! name
79
76
  end
80
77
 
81
- # Defines a fast writer method with a bang (!) suffix for a given
82
- # attribute name. Fast writer methods are used to immediately persist
83
- # attribute values to Redis. Calling a fast writer method has no
84
- # effect on any of the object's other attributes and does not trigger
85
- # a called to update the object's expiration time.
78
+ # Defines a fast attribute method with a bang (!) suffix for a given
79
+ # attribute name. Fast attribute methods are used to immediately read or
80
+ # write attribute values from/to Redis. Calling a fast attribute method
81
+ # has no effect on any of the object's other attributes and does not
82
+ # trigger a call to update the object's expiration time.
86
83
  #
87
84
  # The dynamically defined method performs the following:
88
- # - Checks if the correct number of arguments is provided (exactly one).
85
+ # - Acts as both a reader and a writer method.
86
+ # - When called without arguments, retrieves the current value from Redis.
87
+ # - When called with an argument, persists the value to Redis immediately.
88
+ # - Checks if the correct number of arguments is provided (zero or one).
89
89
  # - Converts the provided value to a format suitable for Redis storage.
90
- # - Uses the existing accessor method to set the attribute value.
91
- # - Persists the value to Redis immediately using the hset command.
92
- # - Includes custom error handling to raise an ArgumentError if the wrong number of arguments is given.
93
- # - Raises a custom error message if an exception occurs during the execution of the method.
94
- #
95
- # @param [Symbol, String] name the name of the attribute for which the writer method is defined.
96
- # @raise [ArgumentError] if the wrong number of arguments is provided.
97
- # @raise [RuntimeError] if an exception occurs during the execution of the method.
98
- #
99
- def fast_writer!(name)
90
+ # - Uses the existing accessor method to set the attribute value when
91
+ # writing.
92
+ # - Persists the value to Redis immediately using the hset command when
93
+ # writing.
94
+ # - Includes custom error handling to raise an ArgumentError if the wrong
95
+ # number of arguments is given.
96
+ # - Raises a custom error message if an exception occurs during the
97
+ # execution of the method.
98
+ #
99
+ # @param [Symbol, String] name the name of the attribute for which the
100
+ # fast method is defined.
101
+ # @return [Object] the current value of the attribute when called without
102
+ # arguments.
103
+ # @raise [ArgumentError] if more than one argument is provided.
104
+ # @raise [RuntimeError] if an exception occurs during the execution of the
105
+ # method.
106
+ #
107
+ def fast_attribute!(name = nil)
108
+ # Fast attribute accessor method for the '#{name}' attribute.
109
+ # This method provides immediate read and write access to the attribute
110
+ # in Redis.
111
+ #
112
+ # When called without arguments, it retrieves the current value of the
113
+ # attribute from Redis.
114
+ # When called with an argument, it immediately persists the new value to
115
+ # Redis.
116
+ #
117
+ # @overload #{name}!
118
+ # Retrieves the current value of the attribute from Redis.
119
+ # @return [Object] the current value of the attribute.
120
+ #
121
+ # @overload #{name}!(value)
122
+ # Sets and immediately persists the new value of the attribute to
123
+ # Redis.
124
+ # @param value [Object] the new value to set for the attribute.
125
+ # @return [Object] the newly set value.
126
+ #
127
+ # @raise [ArgumentError] if more than one argument is provided.
128
+ # @raise [RuntimeError] if an exception occurs during the execution of
129
+ # the method.
130
+ #
131
+ # @note This method bypasses any object-level caching and interacts
132
+ # directly with Redis. It does not trigger updates to other attributes
133
+ # or the object's expiration time.
134
+ #
135
+ # @example
136
+ #
137
+ # def #{name}!(*args)
138
+ # # Method implementation
139
+ # end
140
+ #
100
141
  define_method :"#{name}!" do |*args|
101
142
  # Check if the correct number of arguments is provided (exactly one).
102
- raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)" if args.size != 1
143
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0 or 1)" if args.size > 1
103
144
 
104
145
  val = args.first
105
146
 
147
+ # If no value is provided to this fast attribute method, make a call
148
+ # to redis to return the current stored value of the hash field.
149
+ return hget name if val.nil?
150
+
106
151
  begin
107
152
  # Trace the operation if debugging is enabled.
108
153
  Familia.trace :FAST_WRITER, redis, "#{name}: #{val.inspect}", caller(1..1) if Familia.debug?
109
154
 
110
155
  # Convert the provided value to a format suitable for Redis storage.
111
- prepared = to_redis(val)
112
- Familia.ld "[.fast_writer!] #{name} val: #{val.class} prepared: #{prepared.class}"
156
+ prepared = serialize_value(val)
157
+ Familia.ld "[.fast_attribute!] #{name} val: #{val.class} prepared: #{prepared.class}"
113
158
 
114
159
  # Use the existing accessor method to set the attribute value.
115
160
  send :"#{name}=", val
@@ -148,6 +193,10 @@ module Familia
148
193
  @redis_types
149
194
  end
150
195
 
196
+ def has_relations?
197
+ @has_relations ||= false
198
+ end
199
+
151
200
  def db(v = nil)
152
201
  @db = v unless v.nil?
153
202
  @db || parent&.db
@@ -161,16 +210,20 @@ module Familia
161
210
  def all(suffix = nil)
162
211
  suffix ||= self.suffix
163
212
  # objects that could not be parsed will be nil
164
- keys(suffix).filter_map { |k| from_rediskey(k) }
213
+ keys(suffix).filter_map { |k| find_by_key(k) }
165
214
  end
166
215
 
167
216
  def any?(filter = '*')
168
- size(filter) > 0
217
+ matching_keys_count(filter) > 0
169
218
  end
170
219
 
171
- def size(filter = '*')
220
+ # Returns the number of Redis keys matching the given filter pattern
221
+ # @param filter [String] Redis key pattern to match (default: '*')
222
+ # @return [Integer] Number of matching keys
223
+ def matching_keys_count(filter = '*')
172
224
  redis.keys(rediskey(filter)).compact.size
173
225
  end
226
+ alias size matching_keys_count # For backwards compatibility
174
227
 
175
228
  def suffix(a = nil, &blk)
176
229
  @suffix = a || blk if a || !blk.nil?
@@ -264,17 +317,17 @@ module Familia
264
317
  # debugging.
265
318
  #
266
319
  # @example
267
- # User.from_rediskey("user:123") # Returns a User instance if it exists,
320
+ # User.find_by_key("user:123") # Returns a User instance if it exists,
268
321
  # nil otherwise
269
322
  #
270
- def from_rediskey(objkey)
323
+ def find_by_key(objkey)
271
324
  raise ArgumentError, 'Empty key' if objkey.to_s.empty?
272
325
 
273
326
  # We use a lower-level method here b/c we're working with the
274
327
  # full key and not just the identifier.
275
328
  does_exist = redis.exists(objkey).positive?
276
329
 
277
- Familia.ld "[.from_rediskey] #{self} from key #{objkey} (exists: #{does_exist})"
330
+ Familia.ld "[.find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
278
331
  Familia.trace :FROM_KEY, redis, objkey, caller(1..1) if Familia.debug?
279
332
 
280
333
  # This is the reason for calling exists first. We want to definitively
@@ -289,6 +342,7 @@ module Familia
289
342
 
290
343
  new(**obj)
291
344
  end
345
+ alias from_rediskey find_by_key # deprecated
292
346
 
293
347
  # Retrieves and instantiates an object from Redis using its identifier.
294
348
  #
@@ -299,7 +353,7 @@ module Familia
299
353
  # @return [Object, nil] An instance of the class if found, nil otherwise.
300
354
  #
301
355
  # This method constructs the full Redis key using the provided identifier
302
- # and suffix, then delegates to `from_rediskey` for the actual retrieval and
356
+ # and suffix, then delegates to `find_by_key` for the actual retrieval and
303
357
  # instantiation.
304
358
  #
305
359
  # It's a higher-level method that abstracts away the key construction,
@@ -307,19 +361,21 @@ module Familia
307
361
  # identifier.
308
362
  #
309
363
  # @example
310
- # User.from_identifier(123) # Equivalent to User.from_rediskey("user:123:object")
364
+ # User.find_by_id(123) # Equivalent to User.find_by_key("user:123:object")
311
365
  #
312
- def from_identifier(identifier, suffix = nil)
366
+ def find_by_id(identifier, suffix = nil)
313
367
  suffix ||= self.suffix
314
368
  return nil if identifier.to_s.empty?
315
369
 
316
370
  objkey = rediskey(identifier, suffix)
317
371
 
318
- Familia.ld "[.from_identifier] #{self} from key #{objkey})"
319
- Familia.trace :FROM_IDENTIFIER, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
320
- from_rediskey objkey
372
+ Familia.ld "[.find_by_id] #{self} from key #{objkey})"
373
+ Familia.trace :FIND_BY_ID, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
374
+ find_by_key objkey
321
375
  end
322
- alias load from_identifier
376
+ alias find find_by_id
377
+ alias load find_by_id # deprecated
378
+ alias from_identifier find_by_id # deprecated
323
379
 
324
380
  # Checks if an object with the given identifier exists in Redis.
325
381
  #
@@ -351,8 +407,10 @@ module Familia
351
407
  # @param suffix [Symbol, nil] The suffix to use in the Redis key (default: class suffix).
352
408
  # @return [Boolean] true if the object was successfully destroyed, false otherwise.
353
409
  #
354
- # This method constructs the full Redis key using the provided identifier and suffix,
355
- # then removes the corresponding key from Redis.
410
+ # This method is part of Familia's high-level object lifecycle management. While `delete!`
411
+ # operates directly on Redis keys, `destroy!` operates at the object level and is used for
412
+ # ORM-style operations. Use `destroy!` when removing complete objects from the system, and
413
+ # `delete!` when working directly with Redis keys.
356
414
  #
357
415
  # @example
358
416
  # User.destroy!(123) # Removes user:123:object from Redis
@@ -364,7 +422,7 @@ module Familia
364
422
  objkey = rediskey identifier, suffix
365
423
 
366
424
  ret = redis.del objkey
367
- Familia.trace :DELETED, redis, "#{objkey}: #{ret.inspect}", caller(1..1) if Familia.debug?
425
+ Familia.trace :DESTROY!, redis, "#{objkey} #{ret.inspect}", caller(1..1) if Familia.debug?
368
426
  ret.positive?
369
427
  end
370
428
 
@@ -380,7 +438,7 @@ module Familia
380
438
  # User.find # Returns all keys matching user:*:object
381
439
  # User.find('active') # Returns all keys matching user:*:active
382
440
  #
383
- def find(suffix = '*')
441
+ def find_keys(suffix = '*')
384
442
  redis.keys(rediskey('*', suffix)) || []
385
443
  end
386
444
 
@@ -18,11 +18,35 @@ module Familia
18
18
  #
19
19
  module Commands
20
20
 
21
+ def move(db)
22
+ redis.move rediskey, db
23
+ end
24
+
25
+ # Checks if the calling object's key exists in Redis and has a non-zero size.
26
+ #
27
+ # This method retrieves the Redis URI associated with the calling object's class
28
+ # using `Familia.redis_uri_by_class`. It then checks if the specified key exists
29
+ # in Redis and that its size is not zero. If debugging is enabled, it logs the
30
+ # existence check using `Familia.trace`.
31
+ #
32
+ # @return [Boolean] Returns `true` if the key exists in Redis and its size is not zero, otherwise `false`.
33
+ # @example
34
+ # if some_object.exists?
35
+ # # perform action
36
+ # end
21
37
  def exists?
22
- # Trace output comes from the class method
23
- self.class.exists? identifier, suffix
38
+ true_or_false = self.class.redis.exists?(rediskey) && !size.zero?
39
+ Familia.trace :EXISTS, redis, "#{key} #{true_or_false.inspect}", caller(1..1) if Familia.debug?
40
+ true_or_false
24
41
  end
25
42
 
43
+ # Returns the number of fields in the main object hash
44
+ # @return [Integer] number of fields
45
+ def field_count
46
+ redis.hlen rediskey
47
+ end
48
+ alias size field_count
49
+
26
50
  # Sets a timeout on key. After the timeout has expired, the key will
27
51
  # automatically be deleted. Returns 1 if the timeout was set, 0 if key
28
52
  # does not exist or the timeout could not be set.
@@ -33,20 +57,27 @@ module Familia
33
57
  redis.expire rediskey, ttl.to_i
34
58
  end
35
59
 
60
+ # Retrieves the remaining time to live (TTL) for the object's Redis key.
61
+ #
62
+ # This method accesses the ovjects Redis client to obtain the TTL of `rediskey`.
63
+ # If debugging is enabled, it logs the TTL retrieval operation using `Familia.trace`.
64
+ #
65
+ # @return [Integer] The TTL of the key in seconds. Returns -1 if the key does not exist
66
+ # or has no associated expire time.
36
67
  def realttl
37
68
  Familia.trace :REALTTL, redis, redisuri, caller(1..1) if Familia.debug?
38
69
  redis.ttl rediskey
39
70
  end
40
71
 
41
- # Deletes a field from the hash stored at the Redis key.
72
+ # Removes a field from the hash stored at the Redis key.
42
73
  #
43
- # @param field [String] The field to delete from the hash.
74
+ # @param field [String] The field to remove from the hash.
44
75
  # @return [Integer] The number of fields that were removed from the hash (0 or 1).
45
- # @note This method is destructive, as indicated by the bang (!).
46
- def hdel!(field)
76
+ def remove_field(field)
47
77
  Familia.trace :HDEL, redis, field, caller(1..1) if Familia.debug?
48
78
  redis.hdel rediskey, field
49
79
  end
80
+ alias remove remove_field # deprecated
50
81
 
51
82
  def redistype
52
83
  Familia.trace :REDISTYPE, redis, redisuri, caller(1..1) if Familia.debug?
@@ -59,6 +90,15 @@ module Familia
59
90
  redis.rename rediskey, newkey
60
91
  end
61
92
 
93
+ # Retrieves the prefix for the current instance by delegating to its class.
94
+ #
95
+ # @return [String] The prefix associated with the class of the current instance.
96
+ # @example
97
+ # instance.prefix
98
+ def prefix
99
+ self.class.prefix
100
+ end
101
+
62
102
  # For parity with RedisType#hgetall
63
103
  def hgetall
64
104
  Familia.trace :HGETALL, redis, redisuri, caller(1..1) if Familia.debug?
@@ -78,8 +118,10 @@ module Familia
78
118
  redis.hset rediskey, field, value
79
119
  end
80
120
 
81
- def hmset
82
- redis.hmset rediskey(suffix), self.to_h
121
+ def hmset(hsh={})
122
+ hsh ||= self.to_h
123
+ Familia.trace :HMSET, redis, hsh, caller(1..1) if Familia.debug?
124
+ redis.hmset rediskey(suffix), hsh
83
125
  end
84
126
 
85
127
  def hkeys
@@ -116,11 +158,6 @@ module Familia
116
158
  end
117
159
  alias decrement decr
118
160
 
119
- def hlen
120
- redis.hlen rediskey(suffix)
121
- end
122
- alias hlength hlen
123
-
124
161
  def hstrlen(field)
125
162
  redis.hstrlen rediskey(suffix), field
126
163
  end
@@ -131,12 +168,14 @@ module Familia
131
168
  end
132
169
  alias has_key? key?
133
170
 
171
+ # Deletes the entire Redis key
172
+ # @return [Boolean] true if the key was deleted, false otherwise
134
173
  def delete!
135
174
  Familia.trace :DELETE!, redis, redisuri, caller(1..1) if Familia.debug?
136
175
  ret = redis.del rediskey
137
176
  ret.positive?
138
177
  end
139
- protected :delete!
178
+ alias clear delete!
140
179
 
141
180
  end
142
181
 
@@ -16,6 +16,10 @@ module Familia
16
16
  # Call setup_relations_accessors to initialize the feature
17
17
  #
18
18
  module RelationsManagement
19
+ # A practical flag to indicate that a Horreum member has relations,
20
+ # not just theoretically but actually at least one list/haskey/etc.
21
+ @has_relations = nil
22
+
19
23
  def self.included(base)
20
24
  base.extend(ClassMethods)
21
25
  base.setup_relations_accessors
@@ -31,14 +35,17 @@ module Familia
31
35
 
32
36
  # Dynamically define instance-level relation methods
33
37
  #
34
- # Once defined, these methods can be used at the class-level of a
38
+ # Once defined, these methods can be used at the instance-level of a
35
39
  # Familia member to define *instance-level* relations to any of the
36
40
  # RedisType types (e.g. set, list, hash, etc).
37
41
  #
38
42
  define_method :"#{kind}" do |*args|
39
43
  name, opts = *args
44
+
45
+ # As log as we have at least one relation, we can set this flag.
46
+ @has_relations = true
47
+
40
48
  attach_instance_redis_object_relation name, klass, opts
41
- redis_types[name.to_s.to_sym]
42
49
  end
43
50
  define_method :"#{kind}?" do |name|
44
51
  obj = redis_types[name.to_s.to_sym]