familia 1.2.1 → 2.0.0.pre2

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +68 -0
  3. data/.github/workflows/docs.yml +64 -0
  4. data/.gitignore +4 -0
  5. data/.pre-commit-config.yaml +3 -1
  6. data/.rubocop.yml +16 -9
  7. data/.rubocop_todo.yml +177 -31
  8. data/.yardopts +9 -0
  9. data/CLAUDE.md +141 -0
  10. data/Gemfile +16 -2
  11. data/Gemfile.lock +97 -36
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +192 -0
  15. data/familia.gemspec +10 -6
  16. data/lib/familia/base.rb +19 -9
  17. data/lib/familia/connection.rb +232 -65
  18. data/lib/familia/core_ext.rb +1 -1
  19. data/lib/familia/datatype/commands.rb +59 -0
  20. data/lib/familia/{redistype → datatype}/serialization.rb +9 -13
  21. data/lib/familia/{redistype → datatype}/types/hashkey.rb +25 -25
  22. data/lib/familia/{redistype → datatype}/types/list.rb +13 -13
  23. data/lib/familia/{redistype → datatype}/types/sorted_set.rb +20 -20
  24. data/lib/familia/{redistype → datatype}/types/string.rb +22 -21
  25. data/lib/familia/{redistype → datatype}/types/unsorted_set.rb +11 -11
  26. data/lib/familia/datatype.rb +243 -0
  27. data/lib/familia/errors.rb +5 -2
  28. data/lib/familia/features/expiration.rb +33 -34
  29. data/lib/familia/features/quantization.rb +9 -3
  30. data/lib/familia/features/safe_dump.rb +2 -3
  31. data/lib/familia/features.rb +2 -2
  32. data/lib/familia/horreum/class_methods.rb +97 -110
  33. data/lib/familia/horreum/commands.rb +46 -51
  34. data/lib/familia/horreum/connection.rb +82 -0
  35. data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
  36. data/lib/familia/horreum/serialization.rb +61 -198
  37. data/lib/familia/horreum/settings.rb +6 -17
  38. data/lib/familia/horreum/utils.rb +11 -10
  39. data/lib/familia/horreum.rb +69 -60
  40. data/lib/familia/logging.rb +12 -12
  41. data/lib/familia/multi_result.rb +72 -0
  42. data/lib/familia/refinements.rb +7 -44
  43. data/lib/familia/settings.rb +11 -11
  44. data/lib/familia/utils.rb +123 -90
  45. data/lib/familia/version.rb +4 -21
  46. data/lib/familia.rb +18 -13
  47. data/lib/middleware/database_middleware.rb +150 -0
  48. data/try/configuration/scenarios_try.rb +65 -0
  49. data/try/core/connection_try.rb +58 -0
  50. data/try/core/errors_try.rb +93 -0
  51. data/try/core/extensions_try.rb +26 -0
  52. data/try/{10_familia_try.rb → core/familia_extended_try.rb} +11 -10
  53. data/try/{00_familia_try.rb → core/familia_try.rb} +7 -5
  54. data/try/core/middleware_try.rb +68 -0
  55. data/try/core/refinements_try.rb +39 -0
  56. data/try/core/settings_try.rb +76 -0
  57. data/try/core/tools_try.rb +54 -0
  58. data/try/core/utils_try.rb +189 -0
  59. data/try/{26_redis_bool_try.rb → datatypes/boolean_try.rb} +4 -2
  60. data/try/datatypes/datatype_base_try.rb +69 -0
  61. data/try/{25_redis_type_hash_try.rb → datatypes/hash_try.rb} +5 -3
  62. data/try/{23_redis_type_list_try.rb → datatypes/list_try.rb} +5 -3
  63. data/try/{22_redis_type_set_try.rb → datatypes/set_try.rb} +5 -3
  64. data/try/{21_redis_type_zset_try.rb → datatypes/sorted_set_try.rb} +6 -4
  65. data/try/{24_redis_type_string_try.rb → datatypes/string_try.rb} +8 -8
  66. data/try/edge_cases/empty_identifiers_try.rb +48 -0
  67. data/try/{92_symbolize_try.rb → edge_cases/hash_symbolization_try.rb} +12 -7
  68. data/try/edge_cases/json_serialization_try.rb +85 -0
  69. data/try/edge_cases/race_conditions_try.rb +60 -0
  70. data/try/edge_cases/reserved_keywords_try.rb +59 -0
  71. data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +60 -59
  72. data/try/edge_cases/ttl_side_effects_try.rb +51 -0
  73. data/try/features/expiration_try.rb +86 -0
  74. data/try/features/quantization_try.rb +90 -0
  75. data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
  76. data/try/features/safe_dump_try.rb +137 -0
  77. data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
  78. data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
  79. data/try/horreum/class_methods_try.rb +41 -0
  80. data/try/horreum/commands_try.rb +49 -0
  81. data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
  82. data/try/horreum/relations_try.rb +146 -0
  83. data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
  84. data/try/horreum/settings_try.rb +43 -0
  85. data/try/integration/cross_component_try.rb +46 -0
  86. data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
  87. data/try/{40_customer_try.rb → models/customer_try.rb} +21 -18
  88. data/try/models/datatype_base_try.rb +100 -0
  89. data/try/{30_familia_object_try.rb → models/familia_object_try.rb} +18 -16
  90. data/try/performance/benchmarks_try.rb +55 -0
  91. data/try/pooling/README.md +20 -0
  92. data/try/pooling/configurable_stress_test_try.rb +435 -0
  93. data/try/pooling/connection_pool_test_try.rb +273 -0
  94. data/try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  95. data/try/pooling/lib/connection_pool_metrics.rb +372 -0
  96. data/try/pooling/lib/connection_pool_stress_test.rb +959 -0
  97. data/try/pooling/lib/connection_pool_threading_models.rb +421 -0
  98. data/try/pooling/lib/visualize_stress_results.rb +434 -0
  99. data/try/pooling/pool_siege_try.rb +509 -0
  100. data/try/pooling/run_stress_tests_try.rb +482 -0
  101. data/try/prototypes/atomic_saves_v1_context_proxy.rb +121 -0
  102. data/try/prototypes/atomic_saves_v2_connection_switching.rb +161 -0
  103. data/try/prototypes/atomic_saves_v3_connection_pool.rb +189 -0
  104. data/try/prototypes/atomic_saves_v4.rb +105 -0
  105. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +124 -0
  106. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  107. metadata +143 -46
  108. data/.github/workflows/ruby.yml +0 -71
  109. data/VERSION.yml +0 -4
  110. data/lib/familia/redistype/commands.rb +0 -59
  111. data/lib/familia/redistype.rb +0 -228
  112. data/lib/familia/tools.rb +0 -68
  113. data/lib/redis_middleware.rb +0 -109
  114. data/try/20_redis_type_try.rb +0 -70
  115. data/try/91_json_bug_try.rb +0 -86
@@ -1,4 +1,4 @@
1
- # frozen_string_literal: true
1
+ # lib/familia/horreum.rb
2
2
 
3
3
  module Familia
4
4
  #
@@ -6,8 +6,8 @@ module Familia
6
6
  #
7
7
  # Key features:
8
8
  # * Provides instance-level access to a single hash in Redis
9
- # * Includes Familia for class/module level access to Redis types and operations
10
- # * Uses 'hashkey' to define a Redis hash referred to as "object"
9
+ # * Includes Familia for class/module level access to Database types and operations
10
+ # * Uses 'hashkey' to define a Database hash referred to as "object"
11
11
  # * Applies a default expiry (5 years) to all keys
12
12
  #
13
13
  # Metaprogramming:
@@ -55,13 +55,15 @@ module Familia
55
55
  #
56
56
  class << self
57
57
  attr_accessor :parent
58
- attr_writer :redis, :dump_method, :load_method
58
+ # TODO: Where are we calling dbclient= from now with connection pool?
59
+ attr_writer :dbclient, :dump_method, :load_method
59
60
  attr_reader :has_relations
60
61
 
61
62
  # Extends ClassMethods to subclasses and tracks Familia members
62
63
  def inherited(member)
63
64
  Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
64
65
  member.extend(ClassMethods)
66
+ member.extend(Connection)
65
67
  member.extend(Features)
66
68
 
67
69
  # Tracks all the classes/modules that include Familia. It's
@@ -84,17 +86,8 @@ module Familia
84
86
  Familia.ld "[Horreum] Initializing #{self.class}"
85
87
  initialize_relatives
86
88
 
87
- # Automatically add a 'key' field if it's not already defined. This ensures
88
- # that every object horreum class has a unique identifier field. Ideally
89
- # this logic would live somewhere else b/c we only need to call it once
90
- # per class definition. Here it gets called every time an instance is
91
- # instantiated.
92
- unless self.class.fields.include?(:key)
93
- # Define the 'key' field for this class
94
- # This approach allows flexibility in how identifiers are generated
95
- # while ensuring each object has a consistent way to be referenced
96
- self.class.field :key
97
- end
89
+ # No longer auto-create a key field - the identifier method will
90
+ # directly use the field specified by identifier_field
98
91
 
99
92
  # Detect if first argument is a hash (legacy support)
100
93
  if args.size == 1 && args.first.is_a?(Hash) && kwargs.empty?
@@ -131,7 +124,7 @@ module Familia
131
124
  else
132
125
  Familia.ld "[Horreum] #{self.class} initialized with no arguments"
133
126
  # Default values are intentionally NOT set here to:
134
- # - Maintain Redis memory efficiency (only store non-nil values)
127
+ # - Maintain Database memory efficiency (only store non-nil values)
135
128
  # - Avoid conflicts with nil-skipping serialization logic
136
129
  # - Preserve consistent exists? behavior (empty vs default-filled objects)
137
130
  # - Keep initialization lightweight for unused fields
@@ -143,50 +136,49 @@ module Familia
143
136
  init if respond_to?(:init)
144
137
  end
145
138
 
146
- # Sets up related Redis objects for the instance
139
+ # Sets up related Database objects for the instance
147
140
  # This method is crucial for establishing Redis-based relationships
148
141
  #
149
142
  # This needs to be called in the initialize method.
150
143
  #
151
144
  def initialize_relatives
152
- # Generate instances of each RedisType. These need to be
145
+ # Generate instances of each DataType. These need to be
153
146
  # unique for each instance of this class so they can piggyback
154
147
  # on the specifc index of this instance.
155
148
  #
156
149
  # i.e.
157
- # familia_object.rediskey == v1:bone:INDEXVALUE:object
158
- # familia_object.redis_type.rediskey == v1:bone:INDEXVALUE:name
150
+ # familia_object.dbkey == v1:bone:INDEXVALUE:object
151
+ # familia_object.related_object.dbkey == v1:bone:INDEXVALUE:name
159
152
  #
160
- # See RedisType.install_redis_type
161
- self.class.redis_types.each_pair do |name, redis_type_definition|
162
- klass = redis_type_definition.klass
163
- opts = redis_type_definition.opts
153
+ self.class.related_fields.each_pair do |name, datatype_definition|
154
+ klass = datatype_definition.klass
155
+ opts = datatype_definition.opts
164
156
  Familia.ld "[#{self.class}] initialize_relatives #{name} => #{klass} #{opts.keys}"
165
157
 
166
158
  # As a subclass of Familia::Horreum, we add ourselves as the parent
167
- # automatically. This is what determines the rediskey for RedisType
168
- # instance and which redis connection.
159
+ # automatically. This is what determines the dbkey for DataType
160
+ # instance and which database connection.
169
161
  #
170
- # e.g. If the parent's rediskey is `customer:customer_id:object`
171
- # then the rediskey for this RedisType instance will be
162
+ # e.g. If the parent's dbkey is `customer:customer_id:object`
163
+ # then the dbkey for this DataType instance will be
172
164
  # `customer:customer_id:name`.
173
165
  #
174
166
  opts[:parent] = self # unless opts.key(:parent)
175
167
 
176
168
  suffix_override = opts.fetch(:suffix, name)
177
169
 
178
- # Instantiate the RedisType object and below we store it in
170
+ # Instantiate the DataType object and below we store it in
179
171
  # an instance variable.
180
- redis_type = klass.new suffix_override, opts
172
+ related_object = klass.new suffix_override, opts
181
173
 
182
- # Freezes the redis_type, making it immutable.
174
+ # Freezes the related_object, making it immutable.
183
175
  # This ensures the object's state remains consistent and prevents any modifications,
184
176
  # safeguarding its integrity and making it thread-safe.
185
177
  # Any attempts to change the object after this will raise a FrozenError.
186
- redis_type.freeze
178
+ related_object.freeze
187
179
 
188
180
  # e.g. customer.name #=> `#<Familia::HashKey:0x0000...>`
189
- instance_variable_set :"@#{name}", redis_type
181
+ instance_variable_set :"@#{name}", related_object
190
182
  end
191
183
  end
192
184
 
@@ -198,7 +190,7 @@ module Familia
198
190
  # (i.e., had non-nil values assigned)
199
191
  # @private
200
192
  def initialize_with_positional_args(*args)
201
- Familia.trace :INITIALIZE_ARGS, redis, args, caller(1..1) if Familia.debug?
193
+ Familia.trace :INITIALIZE_ARGS, dbclient, args, caller(1..1) if Familia.debug?
202
194
  self.class.fields.zip(args).filter_map do |field, value|
203
195
  if value
204
196
  send(:"#{field}=", value)
@@ -217,9 +209,9 @@ module Familia
217
209
  # (i.e., had non-nil values assigned)
218
210
  # @private
219
211
  def initialize_with_keyword_args(**fields)
220
- Familia.trace :INITIALIZE_KWARGS, redis, fields.keys, caller(1..1) if Familia.debug?
212
+ Familia.trace :INITIALIZE_KWARGS, dbclient, fields.keys, caller(1..1) if Familia.debug?
221
213
  self.class.fields.filter_map do |field|
222
- # Redis will give us field names as strings back, but internally
214
+ # Database will give us field names as strings back, but internally
223
215
  # we use symbols. So we check for both.
224
216
  value = fields[field.to_sym] || fields[field.to_s]
225
217
  if value
@@ -230,8 +222,8 @@ module Familia
230
222
  end
231
223
  private :initialize_with_keyword_args
232
224
 
233
- def initialize_with_keyword_args_from_redis(**fields)
234
- # Deserialize Redis string values back to their original types
225
+ def initialize_with_keyword_args_deserialize_value(**fields)
226
+ # Deserialize Database string values back to their original types
235
227
  deserialized_fields = fields.transform_values { |value| deserialize_value(value) }
236
228
  initialize_with_keyword_args(**deserialized_fields)
237
229
  end
@@ -240,7 +232,7 @@ module Familia
240
232
  # hash and refreshes the existing object.
241
233
  #
242
234
  # This method is part of horreum.rb rather than serialization.rb because it
243
- # operates solely on the provided values and doesn't query Redis or other
235
+ # operates solely on the provided values and doesn't query Database or other
244
236
  # external sources. That's why it's called "optimistic" refresh: it assumes
245
237
  # the provided values are correct and updates the object accordingly.
246
238
  #
@@ -250,54 +242,71 @@ module Familia
250
242
  # the object with.
251
243
  # @return [Array] The list of field names that were updated.
252
244
  def optimistic_refresh(**fields)
253
- Familia.ld "[optimistic_refresh] #{self.class} #{rediskey} #{fields.keys}"
254
- initialize_with_keyword_args_from_redis(**fields)
245
+ Familia.ld "[optimistic_refresh] #{self.class} #{dbkey} #{fields.keys}"
246
+ initialize_with_keyword_args_deserialize_value(**fields)
255
247
  end
256
248
 
257
249
  # Determines the unique identifier for the instance
258
- # This method is used to generate Redis keys for the object
250
+ # This method is used to generate dbkeys for the object
251
+ # Returns nil for unsaved objects (following standard ORM patterns)
259
252
  def identifier
260
- definition = self.class.identifier # e.g.
261
- # When definition is a symbol or string, assume it's an instance method
262
- # to call on the object to get the unique identifier. When it's a callable
263
- # object, call it with the object as the argument. When it's an array,
264
- # call each method in turn and join the results. When it's nil, raise
265
- # an error
253
+ definition = self.class.identifier_field
254
+ return nil if definition.nil?
255
+
256
+ # Call the identifier field or proc (validation already done at class definition time)
266
257
  unique_id = case definition
267
258
  when Symbol, String
268
259
  send(definition)
269
260
  when Proc
270
261
  definition.call(self)
271
- when Array
272
- Familia.join(definition.map { |method| send(method) })
273
- else
274
- raise Problem, "Invalid identifier definition: #{definition.inspect}"
275
262
  end
276
263
 
277
- # If the unique_id is nil, raise an error
278
- raise Problem, "Identifier is nil for #{self.class}" if unique_id.nil?
279
- raise Problem, 'Identifier is empty' if unique_id.empty?
264
+ # Return nil for unpopulated identifiers (like unsaved ActiveRecord objects)
265
+ # Only raise errors when the identifier is actually needed for Redis operations
266
+ return nil if unique_id.nil? || unique_id.to_s.empty?
280
267
 
281
268
  unique_id
282
269
  end
283
270
 
271
+ attr_writer :dbclient
272
+
273
+ # Summon the mystical Database connection from the depths of instance or class.
274
+ #
275
+ # This method is like a magical divining rod, always pointing to the nearest
276
+ # source of Database goodness. It first checks if we have a personal Redis
277
+ # connection (@dbclient), and if not, it borrows the class's connection.
278
+ #
279
+ # @return [Redis] A shimmering Database connection, ready for your bidding.
280
+ #
281
+ # @example Finding your Database way
282
+ # puts object.dbclient
283
+ # # => #<Redis client v5.4.1 for redis://localhost:6379/0>
284
+ #
285
+ def dbclient
286
+ conn = Fiber[:familia_transaction] || @dbclient || self.class.dbclient
287
+ # conn.select(self.class.logical_database)
288
+ conn
289
+ end
290
+
291
+ def generate_id
292
+ @objid ||= Familia.generate_id # rubocop:disable Naming/MemoizedInstanceVariableName
293
+ end
294
+
284
295
  # The principle is: **If Familia objects have `to_s`, then they should work
285
- # everywhere strings are expected**, including as Redis hash field names.
296
+ # everywhere strings are expected**, including as Database hash field names.
286
297
  def to_s
287
298
  # Enable polymorphic string usage for Familia objects
288
299
  # This allows passing Familia objects directly where strings are expected
289
300
  # without requiring explicit .identifier calls
301
+ return super if identifier.to_s.empty?
290
302
  identifier.to_s
291
- rescue => e
292
- # Fallback for cases where identifier might fail
293
- Familia.ld "[#{self.class}#to_s] Failed to get identifier: #{e.message}"
294
- "#<#{self.class}:0x#{object_id.to_s(16)}>"
295
303
  end
296
304
  end
297
305
  end
298
306
 
299
307
  require_relative 'horreum/class_methods'
300
308
  require_relative 'horreum/commands'
309
+ require_relative 'horreum/connection'
301
310
  require_relative 'horreum/serialization'
302
311
  require_relative 'horreum/settings'
303
312
  require_relative 'horreum/utils'
@@ -1,4 +1,4 @@
1
- # rubocop:disable all
1
+ # lib/familia/logging.rb
2
2
 
3
3
  require 'pathname'
4
4
  require 'logger'
@@ -126,7 +126,7 @@ module Familia
126
126
  #
127
127
  # @param label [Symbol] A label for the trace message (e.g., :EXPAND,
128
128
  # :FROMREDIS, :LOAD, :EXISTS).
129
- # @param redis_instance [Redis, Redis::Future, nil] The Redis instance or
129
+ # @param dbclient [Redis, Redis::Future, nil] The Database instance or
130
130
  # Future being used.
131
131
  # @param ident [String] An identifier or key related to the operation being
132
132
  # traced.
@@ -134,31 +134,31 @@ module Familia
134
134
  # obtained from `caller` or `caller.first`. Default is nil.
135
135
  #
136
136
  # @example
137
- # Familia.trace :LOAD, Familia.redis(uri), objkey, caller(1..1) if
137
+ # Familia.trace :LOAD, Familia.dbclient(uri), objkey, caller(1..1) if
138
138
  # Familia.debug?
139
139
  #
140
140
  # @return [nil]
141
141
  #
142
142
  # @note This method only executes if LoggerTraceRefinement::ENABLED is true.
143
- # @note The redis_instance can be a Redis object, Redis::Future (used in
144
- # pipelined and multi blocks), or nil (when the redis connection isn't
143
+ # @note The dbclient can be a Database object, Redis::Future (used in
144
+ # pipelined and multi blocks), or nil (when the database connection isn't
145
145
  # relevant).
146
146
  #
147
- def trace(label, redis_instance, ident, context = nil)
147
+ def trace(label, dbclient, ident, context = nil)
148
148
  return unless LoggerTraceRefinement::ENABLED
149
149
 
150
- # Usually redis_instance is a Redis object, but it could be
150
+ # Usually dbclient is a Database object, but it could be
151
151
  # a Redis::Future which is what is used inside of pipelined
152
152
  # and multi blocks. In some contexts it's nil where the
153
- # redis connection isn't relevant.
154
- instance_id = if redis_instance
155
- case redis_instance
153
+ # database connection isn't relevant.
154
+ instance_id = if dbclient
155
+ case dbclient
156
156
  when Redis
157
- redis_instance.id.respond_to?(:to_s) ? redis_instance.id.to_s : redis_instance.class.name
157
+ dbclient.id.respond_to?(:to_s) ? dbclient.id.to_s : dbclient.class.name
158
158
  when Redis::Future
159
159
  "Redis::Future"
160
160
  else
161
- redis_instance.class.name
161
+ dbclient.class.name
162
162
  end
163
163
  end
164
164
 
@@ -0,0 +1,72 @@
1
+ # lib/familia/multi_result.rb
2
+
3
+ # The magical MultiResult, keeper of Redis's deepest secrets!
4
+ #
5
+ # This quirky little class wraps up the outcome of a Database "transaction"
6
+ # (or as I like to call it, a "Database dance party") with a bow made of
7
+ # pure Ruby delight. It knows if your commands were successful and
8
+ # keeps the results safe in its pocket dimension.
9
+ #
10
+ # @attr_reader success [Boolean] The golden ticket! True if all your
11
+ # Database wishes came true in the transaction.
12
+ # @attr_reader results [Array<String>] A mystical array of return values,
13
+ # each one a whisper from the Database gods.
14
+ #
15
+ # @example Summoning a MultiResult from the void
16
+ # result = MultiResult.new(true, ["OK", "OK"])
17
+ #
18
+ # @example Divining the success of your Database ritual
19
+ # if result.successful?
20
+ # puts "Huzzah! The Database spirits smile upon you!"
21
+ # else
22
+ # puts "Alas! The Database gremlins have conspired against us!"
23
+ # end
24
+ #
25
+ # @example Peering into the raw essence of results
26
+ # result.results.each_with_index do |value, index|
27
+ # puts "Command #{index + 1} whispered back: #{value}"
28
+ # end
29
+ #
30
+ class MultiResult
31
+ # @return [Boolean] true if all commands in the transaction succeeded,
32
+ # false otherwise
33
+ attr_reader :success
34
+
35
+ # @return [Array<String>] The raw return values from the Database commands
36
+ attr_reader :results
37
+
38
+ # Creates a new MultiResult instance.
39
+ #
40
+ # @param success [Boolean] Whether all commands succeeded
41
+ # @param results [Array<String>] The raw results from Database commands
42
+ def initialize(success, results)
43
+ @success = success
44
+ @results = results
45
+ end
46
+
47
+ # Returns a tuple representing the result of the transaction.
48
+ #
49
+ # @return [Array] A tuple containing the success status and the raw results.
50
+ # The success status is a boolean indicating if all commands succeeded.
51
+ # The raw results is an array of return values from the Database commands.
52
+ #
53
+ # @example
54
+ # [true, ["OK", true, 1]]
55
+ #
56
+ def tuple
57
+ [successful?, results]
58
+ end
59
+ alias to_a tuple
60
+
61
+ def to_h
62
+ { success: successful?, results: results }
63
+ end
64
+
65
+ # Convenient method to check if the commit was successful.
66
+ #
67
+ # @return [Boolean] true if all commands succeeded, false otherwise
68
+ def successful?
69
+ @success
70
+ end
71
+ alias success? successful?
72
+ end
@@ -1,4 +1,4 @@
1
- # frozen_string_literal: true
1
+ # lib/familia/refinements.rb
2
2
 
3
3
  require 'pathname'
4
4
  require 'logger'
@@ -6,44 +6,6 @@ require 'logger'
6
6
  # Controls whether tracing is enabled via an environment variable
7
7
  FAMILIA_TRACE = ENV.fetch('FAMILIA_TRACE', 'false').downcase
8
8
 
9
- # FlexibleHashAccess
10
- #
11
- # This module provides a refinement for the Hash class to allow flexible access
12
- # to hash keys using either strings or symbols interchangeably for reading values.
13
- #
14
- # Note: This refinement only affects reading from the hash. Writing to the hash
15
- # maintains the original key type.
16
- #
17
- # @example Using the refinement
18
- # using FlexibleHashAccess
19
- #
20
- # h = { name: "Alice", "age" => 30 }
21
- # h[:name] # => "Alice"
22
- # h["name"] # => "Alice"
23
- # h[:age] # => 30
24
- # h["age"] # => 30
25
- #
26
- # h["job"] = "Developer"
27
- # h[:job] # => "Developer"
28
- # h["job"] # => "Developer"
29
- #
30
- # h[:salary] = 75000
31
- # h[:salary] # => 75000
32
- # h["salary"] # => nil (original key type is preserved)
33
- #
34
- module FlexibleHashAccess
35
- refine Hash do
36
- ##
37
- # Retrieves a value from the hash using either a string or symbol key.
38
- #
39
- # @param key [String, Symbol] The key to look up
40
- # @return [Object, nil] The value associated with the key, or nil if not found
41
- def [](key)
42
- super(key.to_s) || super(key.to_sym)
43
- end
44
- end
45
- end
46
-
47
9
  # LoggerTraceRefinement
48
10
  #
49
11
  # This module adds a 'trace' log level to the Ruby Logger class.
@@ -62,11 +24,12 @@ end
62
24
  # logger.trace("This is a trace message")
63
25
  #
64
26
  module LoggerTraceRefinement
65
- # Indicates whether trace logging is enabled
66
- ENABLED = %w[1 true yes].include?(FAMILIA_TRACE)
67
-
68
- # The numeric level for trace logging (same as DEBUG)
69
- TRACE = 0 unless defined?(TRACE)
27
+ unless defined?(ENABLED)
28
+ # Indicates whether trace logging is enabled
29
+ ENABLED = %w[1 true yes].include?(FAMILIA_TRACE).freeze
30
+ # The numeric level for trace logging (same as DEBUG)
31
+ TRACE = 0
32
+ end
70
33
 
71
34
  refine Logger do
72
35
  ##
@@ -1,16 +1,16 @@
1
- # rubocop:disable all
1
+ # lib/familia/settings.rb
2
2
 
3
3
  module Familia
4
4
 
5
5
  @delim = ':'
6
6
  @prefix = nil
7
7
  @suffix = :object
8
- @ttl = 0 # see update_expiration. Zero is skip. nil is an exception.
9
- @db = nil
8
+ @default_expiration = 0 # see update_expiration. Zero is skip. nil is an exception.
9
+ @logical_database = nil
10
10
 
11
11
  module Settings
12
12
 
13
- attr_writer :delim, :suffix, :ttl, :db, :prefix
13
+ attr_writer :delim, :suffix, :default_expiration, :logical_database, :prefix
14
14
 
15
15
  def delim(val = nil)
16
16
  @delim = val if val
@@ -27,14 +27,15 @@ module Familia
27
27
  @suffix
28
28
  end
29
29
 
30
- def ttl(v = nil)
31
- @ttl = v unless v.nil?
32
- @ttl
30
+ def default_expiration(v = nil)
31
+ @default_expiration = v unless v.nil?
32
+ @default_expiration
33
33
  end
34
34
 
35
- def db(v = nil)
36
- @db = v unless v.nil?
37
- @db
35
+ def logical_database(v = nil)
36
+ Familia.trace :DB, dbclient, "#{@logical_database} #{v}", caller(1..1) if Familia.debug?
37
+ @logical_database = v unless v.nil?
38
+ @logical_database
38
39
  end
39
40
 
40
41
  # We define this do-nothing method because it reads better
@@ -44,5 +45,4 @@ module Familia
44
45
  end
45
46
 
46
47
  end
47
-
48
48
  end