familia 2.0.0.pre18 → 2.0.0.pre19

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +58 -6
  3. data/CLAUDE.md +34 -9
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +9 -47
  6. data/README.md +39 -0
  7. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
  8. data/changelog.d/20251011_203905_delano_next.rst +30 -0
  9. data/changelog.d/20251011_212633_delano_next.rst +13 -0
  10. data/changelog.d/20251011_221253_delano_next.rst +26 -0
  11. data/docs/guides/feature-expiration.md +18 -18
  12. data/docs/migrating/v2.0.0-pre19.md +197 -0
  13. data/examples/datatype_standalone.rb +281 -0
  14. data/lib/familia/connection/behavior.rb +252 -0
  15. data/lib/familia/connection/handlers.rb +95 -0
  16. data/lib/familia/connection/operation_core.rb +1 -1
  17. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
  18. data/lib/familia/connection/transaction_core.rb +7 -9
  19. data/lib/familia/connection.rb +3 -2
  20. data/lib/familia/data_type/connection.rb +151 -7
  21. data/lib/familia/data_type/database_commands.rb +7 -4
  22. data/lib/familia/data_type/serialization.rb +4 -0
  23. data/lib/familia/data_type/types/hashkey.rb +1 -1
  24. data/lib/familia/errors.rb +51 -14
  25. data/lib/familia/features/expiration/extensions.rb +8 -10
  26. data/lib/familia/features/expiration.rb +19 -19
  27. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +39 -38
  28. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +115 -43
  29. data/lib/familia/features/relationships/indexing.rb +37 -42
  30. data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
  31. data/lib/familia/field_type.rb +2 -1
  32. data/lib/familia/horreum/connection.rb +11 -35
  33. data/lib/familia/horreum/database_commands.rb +129 -10
  34. data/lib/familia/horreum/definition.rb +2 -1
  35. data/lib/familia/horreum/management.rb +21 -15
  36. data/lib/familia/horreum/persistence.rb +190 -66
  37. data/lib/familia/horreum/serialization.rb +3 -0
  38. data/lib/familia/horreum/utils.rb +0 -8
  39. data/lib/familia/horreum.rb +31 -12
  40. data/lib/familia/logging.rb +2 -5
  41. data/lib/familia/settings.rb +7 -7
  42. data/lib/familia/version.rb +1 -1
  43. data/lib/middleware/database_logger.rb +76 -5
  44. data/try/edge_cases/string_coercion_try.rb +4 -4
  45. data/try/features/expiration/expiration_try.rb +1 -1
  46. data/try/features/relationships/indexing_try.rb +28 -4
  47. data/try/features/relationships/relationships_api_changes_try.rb +4 -4
  48. data/try/integration/connection/fiber_context_preservation_try.rb +3 -3
  49. data/try/integration/connection/operation_mode_guards_try.rb +1 -1
  50. data/try/integration/connection/pipeline_fallback_integration_try.rb +12 -12
  51. data/try/integration/create_method_try.rb +22 -22
  52. data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
  53. data/try/integration/data_types/datatype_transactions_try.rb +247 -0
  54. data/try/integration/models/customer_safe_dump_try.rb +5 -1
  55. data/try/integration/models/familia_object_try.rb +1 -1
  56. data/try/integration/persistence_operations_try.rb +162 -10
  57. data/try/unit/data_types/boolean_try.rb +1 -1
  58. data/try/unit/data_types/string_try.rb +1 -1
  59. data/try/unit/horreum/auto_indexing_on_save_try.rb +32 -16
  60. data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
  61. data/try/unit/horreum/base_try.rb +1 -1
  62. data/try/unit/horreum/class_methods_try.rb +2 -2
  63. data/try/unit/horreum/initialization_try.rb +1 -1
  64. data/try/unit/horreum/relations_try.rb +4 -4
  65. data/try/unit/horreum/serialization_try.rb +2 -2
  66. data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
  67. data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
  68. metadata +14 -2
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/familia/connection/behavior.rb
4
+
5
+ module Familia
6
+ module Connection
7
+ # Shared connection behavior for both Horreum and DataType classes
8
+ #
9
+ # This module extracts common connection management functionality that was
10
+ # previously duplicated between Horreum::Connection and DataType::Connection.
11
+ # It provides:
12
+ #
13
+ # * URI normalization with logical_database support
14
+ # * Connection creation methods
15
+ # * Transaction and pipeline execution methods
16
+ # * Consistent connection API across object types
17
+ #
18
+ # Classes including this module must implement:
19
+ # * `dbclient(uri = nil)` - Connection resolution method
20
+ # * `build_connection_chain` (private) - Chain of Responsibility setup
21
+ #
22
+ # @example Basic usage in a class
23
+ # class MyDataStore
24
+ # include Familia::Connection::Behavior
25
+ #
26
+ # def dbclient(uri = nil)
27
+ # @connection_chain ||= build_connection_chain
28
+ # @connection_chain.handle(uri)
29
+ # end
30
+ #
31
+ # private
32
+ #
33
+ # def build_connection_chain
34
+ # # ... handler setup ...
35
+ # end
36
+ # end
37
+ #
38
+ module Behavior
39
+ def self.included(base)
40
+ base.class_eval do
41
+ attr_writer :dbclient
42
+ attr_reader :uri
43
+ end
44
+ end
45
+
46
+ # Normalizes various URI formats to a consistent URI object
47
+ #
48
+ # Handles multiple input types and considers the logical_database setting
49
+ # when uri is nil or Integer. This method is public so connection handlers
50
+ # can use it for consistent URI processing.
51
+ #
52
+ # @param uri [Integer, String, URI, nil] The URI to normalize
53
+ # @return [URI] Normalized URI object
54
+ # @raise [ArgumentError] If URI type is invalid
55
+ #
56
+ # @example Integer database number
57
+ # normalize_uri(2) # => URI with db=2 on default server
58
+ #
59
+ # @example String URI
60
+ # normalize_uri('redis://localhost:6379/1')
61
+ #
62
+ # @example nil with logical_database
63
+ # class MyModel
64
+ # include Familia::Connection::Behavior
65
+ # attr_accessor :logical_database
66
+ # end
67
+ # model = MyModel.new
68
+ # model.logical_database = 3
69
+ # model.normalize_uri(nil) # => URI with db=3
70
+ #
71
+ def normalize_uri(uri)
72
+ case uri
73
+ when Integer
74
+ new_uri = Familia.uri.dup
75
+ new_uri.db = uri
76
+ new_uri
77
+ when ->(obj) { obj.is_a?(String) || obj.instance_of?(::String) }
78
+ URI.parse(uri)
79
+ when URI
80
+ uri
81
+ when nil
82
+ # Use logical_database if available, otherwise fall back to Familia.uri
83
+ if respond_to?(:logical_database) && logical_database
84
+ new_uri = Familia.uri.dup
85
+ new_uri.db = logical_database
86
+ new_uri
87
+ else
88
+ Familia.uri
89
+ end
90
+ else
91
+ raise ArgumentError, "Invalid URI type: #{uri.class.name}"
92
+ end
93
+ end
94
+
95
+ # Creates a new Database connection instance
96
+ #
97
+ # This method always creates a fresh connection and does not use caching.
98
+ # Each call returns a new Redis client instance that you are responsible
99
+ # for managing and closing when done.
100
+ #
101
+ # @param uri [String, URI, Integer, nil] The URI of the Database server
102
+ # @return [Redis] A new Database client connection
103
+ #
104
+ # @example Creating a new connection
105
+ # client = create_dbclient('redis://localhost:6379/1')
106
+ # client.ping
107
+ # client.close
108
+ #
109
+ def create_dbclient(uri = nil)
110
+ parsed_uri = normalize_uri(uri)
111
+ Familia.create_dbclient(parsed_uri)
112
+ end
113
+
114
+ # Alias for create_dbclient (backward compatibility)
115
+ def connect(*)
116
+ create_dbclient(*)
117
+ end
118
+
119
+ # Sets the URI for this object's database connection
120
+ #
121
+ # @param uri [String, URI, Integer] The new URI
122
+ # @return [URI] The normalized URI
123
+ #
124
+ def uri=(uri)
125
+ @uri = normalize_uri(uri)
126
+ end
127
+
128
+ # Alias for uri (backward compatibility)
129
+ def url
130
+ uri
131
+ end
132
+
133
+ # Alias for uri= (backward compatibility)
134
+ def url=(uri)
135
+ self.uri = uri
136
+ end
137
+
138
+ # Executes a Redis transaction (MULTI/EXEC) using this object's connection context
139
+ #
140
+ # Provides atomic execution of multiple Redis commands with automatic connection
141
+ # management and operation mode enforcement. Uses the object's database and
142
+ # connection settings. Returns a MultiResult object for consistency.
143
+ #
144
+ # @yield [Redis] conn The Redis connection configured for transaction mode
145
+ # @return [MultiResult] Result object with success status and command results
146
+ #
147
+ # @raise [Familia::OperationModeError] When called with incompatible connection handlers
148
+ #
149
+ # @example Basic transaction
150
+ # obj.transaction do |conn|
151
+ # conn.set('key1', 'value1')
152
+ # conn.set('key2', 'value2')
153
+ # conn.get('key1')
154
+ # end
155
+ #
156
+ # @example Reentrant behavior
157
+ # obj.transaction do |conn|
158
+ # conn.set('outer', 'value')
159
+ #
160
+ # # Nested transaction reuses same connection
161
+ # obj.transaction do |inner_conn|
162
+ # inner_conn.set('inner', 'value')
163
+ # end
164
+ # end
165
+ #
166
+ # @note Connection Inheritance:
167
+ # - Uses object's logical_database setting if configured
168
+ # - Inherits class-level database settings
169
+ # - Falls back to instance-level dbclient if set
170
+ # - Uses global connection chain as final fallback
171
+ #
172
+ # @note Transaction Context:
173
+ # - When called outside global transaction: Creates local MultiResult
174
+ # - When called inside global transaction: Yields to existing transaction
175
+ # - Maintains proper Fiber-local state for nested calls
176
+ #
177
+ # @see Familia.transaction For global transaction method
178
+ # @see MultiResult For details on the return value structure
179
+ #
180
+ def transaction(&)
181
+ ensure_relatives_initialized! if respond_to?(:ensure_relatives_initialized!, true)
182
+ Familia::Connection::TransactionCore.execute_transaction(-> { dbclient }, &)
183
+ end
184
+
185
+ # Alias for transaction (alternate naming)
186
+ def multi(&)
187
+ transaction(&)
188
+ end
189
+
190
+ # Executes Redis commands in a pipeline using this object's connection context
191
+ #
192
+ # Batches multiple Redis commands together and sends them in a single network
193
+ # round-trip for improved performance. Uses the object's database and connection
194
+ # settings. Returns a MultiResult object for consistency.
195
+ #
196
+ # @yield [Redis] conn The Redis connection configured for pipelined mode
197
+ # @return [MultiResult] Result object with success status and command results
198
+ #
199
+ # @raise [Familia::OperationModeError] When called with incompatible connection handlers
200
+ #
201
+ # @example Basic pipeline
202
+ # obj.pipelined do |conn|
203
+ # conn.set('key1', 'value1')
204
+ # conn.incr('counter')
205
+ # conn.get('key1')
206
+ # end
207
+ #
208
+ # @example Performance optimization
209
+ # # Instead of multiple round-trips:
210
+ # obj.save # Round-trip 1
211
+ # obj.increment_count # Round-trip 2
212
+ # obj.update_timestamp # Round-trip 3
213
+ #
214
+ # # Use pipeline for single round-trip:
215
+ # obj.pipelined do |conn|
216
+ # conn.hmset(obj.dbkey, obj.to_h)
217
+ # conn.hincrby(obj.dbkey, 'count', 1)
218
+ # conn.hset(obj.dbkey, 'updated_at', Time.now.to_i)
219
+ # end
220
+ #
221
+ # @note Connection Inheritance:
222
+ # - Uses object's logical_database setting if configured
223
+ # - Inherits class-level database settings
224
+ # - Falls back to instance-level dbclient if set
225
+ # - Uses global connection chain as final fallback
226
+ #
227
+ # @note Pipeline Context:
228
+ # - When called outside global pipeline: Creates local MultiResult
229
+ # - When called inside global pipeline: Yields to existing pipeline
230
+ # - Maintains proper Fiber-local state for nested calls
231
+ #
232
+ # @note Performance Considerations:
233
+ # - Best for multiple independent operations
234
+ # - Reduces network latency by batching commands
235
+ # - Commands execute independently (some may succeed, others fail)
236
+ #
237
+ # @see Familia.pipelined For global pipeline method
238
+ # @see MultiResult For details on the return value structure
239
+ # @see #transaction For atomic command execution
240
+ #
241
+ def pipelined(&block)
242
+ ensure_relatives_initialized! if respond_to?(:ensure_relatives_initialized!, true)
243
+ Familia::Connection::PipelineCore.execute_pipeline(-> { dbclient }, &block)
244
+ end
245
+
246
+ # Alias for pipelined (alternate naming)
247
+ def pipeline(&block)
248
+ pipelined(&block)
249
+ end
250
+ end
251
+ end
252
+ end
@@ -219,5 +219,100 @@ module Familia
219
219
  dbclient
220
220
  end
221
221
  end
222
+
223
+ # Handler for delegating connection resolution to parent object
224
+ #
225
+ # Used by DataType objects that are attached to a parent (Horreum instance or class).
226
+ # Delegates the connection resolution to the parent's dbclient method, which allows
227
+ # DataType objects to inherit connection settings, logical_database, and transaction
228
+ # context from their parent.
229
+ #
230
+ # This preserves the existing architectural pattern where DataType objects owned by
231
+ # Horreum models use the parent's connection chain. This is the primary behavior
232
+ # for DataType objects in typical usage.
233
+ #
234
+ # @example Instance-level DataType with parent
235
+ # user = User.new(userid: 'user_123')
236
+ # user.tags # DataType that delegates to user.dbclient
237
+ #
238
+ # @example Class-level DataType with parent
239
+ # User.global_users # DataType that delegates to User.dbclient
240
+ #
241
+ class ParentDelegationHandler < BaseConnectionHandler
242
+ @allows_transaction = true
243
+ @allows_pipelined = true
244
+
245
+ def initialize(data_type)
246
+ @data_type = data_type
247
+ end
248
+
249
+ def handle(uri)
250
+ return nil unless @data_type.parent
251
+
252
+ # Delegate to parent's connection chain
253
+ # Parent can be either a Horreum class or instance
254
+ parent_connection = @data_type.parent.dbclient(uri)
255
+
256
+ if parent_connection
257
+ Familia.trace :DBCLIENT_PARENT_DELEGATION, @data_type.dbkey,
258
+ "Using parent connection from #{@data_type.parent.class}"
259
+ end
260
+
261
+ parent_connection
262
+ end
263
+ end
264
+
265
+ # Handler for standalone DataType objects without a parent
266
+ #
267
+ # Provides connection resolution for DataType objects that are created independently
268
+ # rather than being attached to a Horreum model. Checks for instance-level @dbclient
269
+ # first, then falls back to creating a connection based on logical_database option
270
+ # or global Familia connection.
271
+ #
272
+ # This enables standalone DataType usage patterns like Rack::Session implementations
273
+ # where DataType objects need independent connection management and transaction support.
274
+ #
275
+ # @example Standalone DataType with custom connection
276
+ # leaderboard = Familia::SortedSet.new('game:leaderboard')
277
+ # leaderboard.dbclient = ConnectionPool.new { Redis.new }
278
+ #
279
+ # @example Standalone DataType with logical_database option
280
+ # cache = Familia::HashKey.new('app:cache', logical_database: 2)
281
+ #
282
+ class StandaloneConnectionHandler < BaseConnectionHandler
283
+ @allows_transaction = true
284
+ @allows_pipelined = true
285
+
286
+ def initialize(data_type)
287
+ @data_type = data_type
288
+ end
289
+
290
+ def handle(uri)
291
+ # If a specific URI is provided, always use it to get a connection.
292
+ if uri
293
+ connection = Familia.dbclient(uri)
294
+ Familia.trace :DBCLIENT_STANDALONE_DATATYPE, @data_type.dbkey,
295
+ "Created standalone connection for specific URI: #{uri}"
296
+ return connection
297
+ end
298
+
299
+ # Use instance @dbclient if explicitly set and no URI was passed
300
+ instance_dbclient = @data_type.instance_variable_get(:@dbclient)
301
+ if instance_dbclient
302
+ Familia.trace :DBCLIENT_DATATYPE_INSTANCE, @data_type.dbkey,
303
+ 'Using DataType instance @dbclient'
304
+ return instance_dbclient
305
+ end
306
+
307
+ # Fall back to creating connection based on opts or global
308
+ target_uri = @data_type.opts[:logical_database]
309
+ connection = Familia.dbclient(target_uri)
310
+
311
+ Familia.trace :DBCLIENT_STANDALONE_DATATYPE, @data_type.dbkey,
312
+ "Created standalone connection for #{target_uri || 'default'}"
313
+
314
+ connection
315
+ end
316
+ end
222
317
  end
223
318
  end
@@ -45,7 +45,7 @@ module Familia
45
45
  when :transaction
46
46
  Familia.transaction_mode
47
47
  when :pipeline
48
- Familia.pipeline_mode
48
+ Familia.pipelined_mode
49
49
  else
50
50
  :strict
51
51
  end
@@ -16,7 +16,7 @@ module Familia
16
16
  #
17
17
  # Handles pipeline execution based on connection handler capabilities.
18
18
  # When handler doesn't support pipelines, fallback behavior is controlled
19
- # by Familia.pipeline_mode setting.
19
+ # by Familia.pipelined_mode setting.
20
20
  #
21
21
  # @param dbclient_proc [Proc] Lambda that returns the Redis connection
22
22
  # @param block [Proc] Block containing Redis commands to execute
@@ -32,7 +32,7 @@ module Familia
32
32
  # result.results # => ["OK", 1]
33
33
  #
34
34
  # @example With fallback modes
35
- # Familia.configure { |c| c.pipeline_mode = :permissive }
35
+ # Familia.configure { |c| c.pipelined_mode = :permissive }
36
36
  # result = PipelineCore.execute_pipeline(-> { cached_conn }) do |conn|
37
37
  # conn.set('key', 'value') # Executes individually, no error
38
38
  # end
@@ -34,27 +34,25 @@ module Familia
34
34
  # result.successful? # => true/false
35
35
  # result.results # => ["OK", 1]
36
36
  #
37
- def self.execute_transaction(dbclient_proc, &block)
37
+ def self.execute_transaction(dbclient_proc, &)
38
38
  # First, get the connection to populate the handler class
39
- connection = dbclient_proc.call
39
+ dbclient_proc.call
40
40
  handler_class = Fiber[:familia_connection_handler_class]
41
41
 
42
42
  # Check transaction capability
43
43
  transaction_capability = handler_class&.allows_transaction
44
44
 
45
45
  if transaction_capability == false
46
- handle_transaction_fallback(dbclient_proc, handler_class, &block)
46
+ handle_transaction_fallback(dbclient_proc, handler_class, &)
47
47
  elsif transaction_capability == :reentrant
48
48
  # Already in transaction, just yield the connection
49
49
  yield(Fiber[:familia_transaction])
50
50
  else
51
51
  # Normal transaction flow (includes nil, true, and other values)
52
- execute_normal_transaction(dbclient_proc, &block)
52
+ execute_normal_transaction(dbclient_proc, &)
53
53
  end
54
54
  end
55
55
 
56
- private
57
-
58
56
  # Handles transaction fallback based on configured transaction mode
59
57
  #
60
58
  # Delegates to OperationCore.handle_fallback for consistent behavior
@@ -65,8 +63,8 @@ module Familia
65
63
  # @param block [Proc] Block containing Redis commands to execute
66
64
  # @return [MultiResult] Result from individual command execution or raises error
67
65
  #
68
- def self.handle_transaction_fallback(dbclient_proc, handler_class, &block)
69
- OperationCore.handle_fallback(:transaction, dbclient_proc, handler_class, &block)
66
+ def self.handle_transaction_fallback(dbclient_proc, handler_class, &)
67
+ OperationCore.handle_fallback(:transaction, dbclient_proc, handler_class, &)
70
68
  end
71
69
 
72
70
  # Executes a normal Redis transaction using MULTI/EXEC
@@ -78,7 +76,7 @@ module Familia
78
76
  # @param block [Proc] Block containing Redis commands to execute
79
77
  # @return [MultiResult] Result object with transaction command results
80
78
  #
81
- def self.execute_normal_transaction(dbclient_proc, &block)
79
+ def self.execute_normal_transaction(dbclient_proc)
82
80
  # Check for existing transaction context
83
81
  return yield(Fiber[:familia_transaction]) if Fiber[:familia_transaction]
84
82
 
@@ -1,12 +1,13 @@
1
1
  # lib/familia/connection.rb
2
2
 
3
+ require_relative 'connection/behavior'
3
4
  require_relative 'connection/handlers'
4
5
  require_relative 'connection/middleware'
5
6
  require_relative 'connection/operations'
6
7
  require_relative 'connection/individual_command_proxy'
7
8
  require_relative 'connection/operation_core'
8
9
  require_relative 'connection/transaction_core'
9
- require_relative 'connection/pipeline_core'
10
+ require_relative 'connection/pipelined_core'
10
11
 
11
12
  # Familia
12
13
  #
@@ -42,7 +43,7 @@ module Familia
42
43
  @connection_chain = nil # Force rebuild of chain
43
44
  end
44
45
 
45
- # Sets the default URI for Database connections.
46
+ # Sets the default URI for Database connections.
46
47
  #
47
48
  # NOTE: uri is not a property of the Settings module b/c it's not
48
49
  # configured in class defintions like default_expiration or logical DB index.
@@ -5,21 +5,104 @@ module Familia
5
5
  # Connection - Instance-level connection and key generation methods
6
6
  #
7
7
  # This module provides instance methods for database connection resolution
8
- # and Redis key generation for DataType objects.
8
+ # and Redis key generation for DataType objects. It includes shared connection
9
+ # behavior from Familia::Connection::Behavior, enabling transaction and pipeline
10
+ # support for both parent-owned and standalone DataType objects.
9
11
  #
10
12
  # Key features:
11
13
  # * Database connection resolution with Chain of Responsibility pattern
12
14
  # * Redis key generation based on parent context
13
15
  # * Direct database access for advanced operations
16
+ # * Transaction support (MULTI/EXEC) for atomic operations
17
+ # * Pipeline support for batched command execution
18
+ # * Parent delegation for owned DataType objects
19
+ # * Standalone connection management for independent DataType objects
20
+ #
21
+ # Connection Chain Priority:
22
+ # 1. FiberTransactionHandler - Active transaction context
23
+ # 2. FiberConnectionHandler - Fiber-local connections
24
+ # 3. ProviderConnectionHandler - User-defined connection provider
25
+ # 4. ParentDelegationHandler - Delegate to parent object (primary for owned DataTypes)
26
+ # 5. StandaloneConnectionHandler - Independent DataType connection
27
+ #
28
+ # @example Parent-owned DataType (automatic delegation)
29
+ # class User < Familia::Horreum
30
+ # logical_database 2
31
+ # zset :scores
32
+ # end
33
+ #
34
+ # user = User.new(userid: 'user_123')
35
+ # user.scores.transaction do |conn|
36
+ # conn.zadd(user.scores.dbkey, 100, 'level1')
37
+ # conn.zadd(user.scores.dbkey, 200, 'level2')
38
+ # end
39
+ #
40
+ # @example Standalone DataType with transaction
41
+ # leaderboard = Familia::SortedSet.new('game:leaderboard')
42
+ # leaderboard.transaction do |conn|
43
+ # conn.zadd(leaderboard.dbkey, 500, 'player1')
44
+ # conn.zadd(leaderboard.dbkey, 600, 'player2')
45
+ # end
14
46
  #
15
47
  module Connection
16
- # TODO: Replace with Chain of Responsibility pattern
17
- def dbclient
48
+ include Familia::Connection::Behavior
49
+
50
+ # Returns the effective URI this DataType will use for connections
51
+ #
52
+ # For parent-owned DataTypes, delegates to parent's URI.
53
+ # For standalone DataTypes with logical_database option, constructs URI with that database.
54
+ # For standalone DataTypes without options, returns global Familia.uri.
55
+ # Explicit @uri assignment (via uri=) takes precedence.
56
+ #
57
+ # @return [URI, nil] The URI for database connections
58
+ #
59
+ def uri
60
+ return @uri if defined?(@uri) && @uri
61
+ return parent.uri if parent && parent.respond_to?(:uri)
62
+
63
+ # Check opts[:logical_database] first, then parent's logical_database
64
+ db_num = opts[:logical_database]
65
+ db_num ||= parent.logical_database if parent && parent.respond_to?(:logical_database)
66
+
67
+ if db_num
68
+ # Create a new URI with the database number but without custom port
69
+ # This ensures consistent URI representation (e.g., redis://host/db not redis://host:port/db)
70
+ base_uri = Familia.uri
71
+ URI.parse("redis://#{base_uri.host}/#{db_num}")
72
+ else
73
+ Familia.uri
74
+ end
75
+ end
76
+
77
+ # Retrieves a Database connection using the Chain of Responsibility pattern
78
+ #
79
+ # Implements connection resolution optimized for DataType usage patterns:
80
+ # - Fast path check for active transaction context
81
+ # - Full connection chain for comprehensive resolution
82
+ # - Parent delegation as primary behavior for owned DataTypes
83
+ # - Standalone connection handling for independent DataTypes
84
+ #
85
+ # Note: We don't cache the connection chain in an instance variable because
86
+ # DataType objects are frozen for thread safety. Building the chain is cheap
87
+ # (just creating handler objects), and the actual connection resolution work
88
+ # is done by the handlers themselves.
89
+ #
90
+ # @param uri [String, URI, Integer, nil] Optional URI for database selection
91
+ # @return [Redis] The Database client for the specified URI
92
+ #
93
+ # @example Getting connection from parent-owned DataType
94
+ # user.tags.dbclient # Delegates to user.dbclient
95
+ #
96
+ # @example Getting connection from standalone DataType
97
+ # cache = Familia::HashKey.new('app:cache', logical_database: 2)
98
+ # cache.dbclient # Uses standalone handler with db 2
99
+ #
100
+ def dbclient(uri = nil)
101
+ # Fast path for transaction context (highest priority)
18
102
  return Fiber[:familia_transaction] if Fiber[:familia_transaction]
19
- return @dbclient if @dbclient
20
103
 
21
- # Delegate to parent if present, otherwise fall back to Familia
22
- parent ? parent.dbclient : Familia.dbclient(opts[:logical_database])
104
+ # Build connection chain (not cached due to frozen objects)
105
+ build_connection_chain.handle(uri)
23
106
  end
24
107
 
25
108
  # Produces the full dbkey for this object.
@@ -75,8 +158,69 @@ module Familia
75
158
  # Provides a structured way to "gear down" to run db commands that are
76
159
  # not implemented in our DataType classes since we intentionally don't
77
160
  # have a method_missing method.
161
+ #
162
+ # Enhanced to work seamlessly with transactions and pipelines. When called
163
+ # within a transaction or pipeline context, uses that connection automatically.
164
+ #
165
+ # @yield [Redis, String] Yields the connection and dbkey to the block
166
+ # @return The return value of the block
167
+ #
168
+ # @example Basic usage
169
+ # datatype.direct_access do |conn, key|
170
+ # conn.zadd(key, 100, 'member')
171
+ # end
172
+ #
173
+ # @example Within transaction (automatic context detection)
174
+ # datatype.transaction do |trans_conn|
175
+ # datatype.direct_access do |conn, key|
176
+ # # conn is the same as trans_conn
177
+ # conn.zadd(key, 200, 'member')
178
+ # end
179
+ # end
180
+ #
78
181
  def direct_access
79
- yield(dbclient, dbkey)
182
+ if Fiber[:familia_transaction]
183
+ # Already in transaction, use that connection
184
+ yield(Fiber[:familia_transaction], dbkey)
185
+ elsif Fiber[:familia_pipeline]
186
+ # Already in pipeline, use that connection
187
+ yield(Fiber[:familia_pipeline], dbkey)
188
+ else
189
+ yield(dbclient, dbkey)
190
+ end
191
+ end
192
+
193
+ private
194
+
195
+ # Builds the connection chain with handlers in priority order
196
+ #
197
+ # Creates the Chain of Responsibility for connection resolution with
198
+ # DataType-specific handlers. Handlers are checked in order:
199
+ #
200
+ # 1. FiberTransactionHandler - Return active transaction connection
201
+ # 2. FiberConnectionHandler - Use fiber-local connection
202
+ # 3. ProviderConnectionHandler - Delegate to connection provider
203
+ # 4. ParentDelegationHandler - Delegate to parent's connection (primary for owned DataTypes)
204
+ # 5. StandaloneConnectionHandler - Handle standalone DataTypes
205
+ #
206
+ # @return [ResponsibilityChain] Configured connection chain
207
+ #
208
+ def build_connection_chain
209
+ # Create fresh handler instances each time since DataType objects are frozen
210
+ # The chain itself is cached in @connection_chain, so this only runs once
211
+ fiber_connection_handler = Familia::Connection::FiberConnectionHandler.new
212
+ provider_connection_handler = Familia::Connection::ProviderConnectionHandler.new
213
+
214
+ # DataType-specific handlers for parent delegation and standalone usage
215
+ parent_delegation_handler = Familia::Connection::ParentDelegationHandler.new(self)
216
+ standalone_connection_handler = Familia::Connection::StandaloneConnectionHandler.new(self)
217
+
218
+ Familia::Connection::ResponsibilityChain.new
219
+ .add_handler(Familia::Connection::FiberTransactionHandler.instance)
220
+ .add_handler(fiber_connection_handler)
221
+ .add_handler(provider_connection_handler)
222
+ .add_handler(parent_delegation_handler)
223
+ .add_handler(standalone_connection_handler)
80
224
  end
81
225
  end
82
226
  end
@@ -22,11 +22,14 @@ module Familia
22
22
  end
23
23
 
24
24
  # Deletes the entire dbkey
25
- # @return [Boolean] true if the key was deleted, false otherwise
25
+ #
26
+ # We return the dbclient.del command's return value instead of a friendly
27
+ # boolean b/c that logic doesn't work inside of a transaction. The return
28
+ # value in that case is a Redis::Future which based on the name indicates
29
+ # that the commend hasn't even run yet.
26
30
  def delete!
27
- Familia.trace :DELETE!, nil, uri if Familia.debug?
28
- ret = dbclient.del dbkey
29
- ret.positive?
31
+ Familia.trace :DELETE!, nil, self.class.uri if Familia.debug?
32
+ dbclient.del dbkey
30
33
  end
31
34
  alias clear delete!
32
35
 
@@ -117,7 +117,11 @@ module Familia
117
117
  # for serialization since everything becomes a string in Valkey.
118
118
  #
119
119
  def deserialize_value(val)
120
+ # Handle Redis::Future objects during transactions first
121
+ return val if val.is_a?(Redis::Future)
122
+
120
123
  return @opts[:default] if val.nil?
124
+
121
125
  return val unless @opts[:class]
122
126
 
123
127
  ret = deserialize_values val
@@ -152,7 +152,7 @@ module Familia
152
152
  # puts "Oops! Our hash seems to have vanished into the Database void!"
153
153
  # end
154
154
  def refresh!
155
- Familia.trace :REFRESH, nil, uri if Familia.debug?
155
+ Familia.trace :REFRESH, nil, self.class.uri if Familia.debug?
156
156
  raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
157
157
 
158
158
  fields = hgetall