familia 2.0.0.pre17 → 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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +118 -6
  3. data/CLAUDE.md +43 -11
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +9 -47
  6. data/README.md +52 -0
  7. data/bin/irb +1 -1
  8. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
  9. data/changelog.d/20251011_203905_delano_next.rst +30 -0
  10. data/changelog.d/20251011_212633_delano_next.rst +13 -0
  11. data/changelog.d/20251011_221253_delano_next.rst +26 -0
  12. data/docs/guides/core-field-system.md +48 -26
  13. data/docs/guides/feature-expiration.md +18 -18
  14. data/docs/migrating/v2.0.0-pre18.md +58 -0
  15. data/docs/migrating/v2.0.0-pre19.md +197 -0
  16. data/docs/qodo-merge-compliance.md +96 -0
  17. data/examples/datatype_standalone.rb +281 -0
  18. data/lib/familia/base.rb +0 -2
  19. data/lib/familia/connection/behavior.rb +252 -0
  20. data/lib/familia/connection/handlers.rb +95 -0
  21. data/lib/familia/connection/middleware.rb +58 -4
  22. data/lib/familia/connection/operation_core.rb +1 -1
  23. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
  24. data/lib/familia/connection/transaction_core.rb +7 -9
  25. data/lib/familia/connection.rb +2 -1
  26. data/lib/familia/data_type/connection.rb +151 -7
  27. data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
  28. data/lib/familia/data_type/serialization.rb +9 -5
  29. data/lib/familia/data_type/types/hashkey.rb +1 -1
  30. data/lib/familia/data_type.rb +2 -2
  31. data/lib/familia/encryption/encrypted_data.rb +12 -2
  32. data/lib/familia/encryption/manager.rb +11 -4
  33. data/lib/familia/errors.rb +51 -14
  34. data/lib/familia/features/autoloader.rb +3 -1
  35. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  36. data/lib/familia/features/expiration/extensions.rb +8 -10
  37. data/lib/familia/features/expiration.rb +19 -19
  38. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
  39. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
  40. data/lib/familia/features/relationships/indexing.rb +37 -42
  41. data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
  42. data/lib/familia/features/safe_dump.rb +2 -3
  43. data/lib/familia/field_type.rb +2 -1
  44. data/lib/familia/horreum/connection.rb +11 -35
  45. data/lib/familia/horreum/database_commands.rb +130 -11
  46. data/lib/familia/horreum/definition.rb +8 -38
  47. data/lib/familia/horreum/management.rb +38 -27
  48. data/lib/familia/horreum/persistence.rb +191 -67
  49. data/lib/familia/horreum/serialization.rb +94 -73
  50. data/lib/familia/horreum/utils.rb +0 -8
  51. data/lib/familia/horreum.rb +41 -18
  52. data/lib/familia/identifier_extractor.rb +60 -0
  53. data/lib/familia/logging.rb +268 -112
  54. data/lib/familia/refinements.rb +0 -1
  55. data/lib/familia/settings.rb +7 -7
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -2
  58. data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
  59. data/pr_agent.toml +31 -0
  60. data/pr_compliance_checklist.yaml +45 -0
  61. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  62. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  63. data/try/edge_cases/json_serialization_try.rb +2 -2
  64. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  65. data/try/edge_cases/race_conditions_try.rb +1 -1
  66. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  67. data/try/edge_cases/string_coercion_try.rb +5 -5
  68. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  69. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  70. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  71. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  72. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  73. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  74. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  75. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  76. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  77. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  78. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  79. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  80. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  81. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  82. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  83. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  84. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  85. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  86. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  87. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  88. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  89. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  90. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  91. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  92. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  93. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  94. data/try/features/expiration/expiration_try.rb +2 -2
  95. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  96. data/try/features/feature_dependencies_try.rb +1 -1
  97. data/try/features/feature_improvements_try.rb +1 -1
  98. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  99. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  100. data/try/features/quantization/quantization_try.rb +1 -1
  101. data/try/features/real_feature_integration_try.rb +17 -14
  102. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  103. data/try/features/relationships/indexing_try.rb +34 -5
  104. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  105. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  106. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  107. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  108. data/try/features/relationships/relationships_api_changes_try.rb +5 -5
  109. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  110. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  111. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  112. data/try/features/relationships/relationships_performance_try.rb +1 -1
  113. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  114. data/try/features/relationships/relationships_try.rb +1 -1
  115. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  116. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  117. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  118. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  119. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  120. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  121. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  122. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
  123. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  124. data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
  125. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  126. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
  127. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
  128. data/try/{core → integration/connection}/pools_try.rb +1 -1
  129. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  130. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  131. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  132. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  133. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  134. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  135. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  136. data/try/{core → integration}/create_method_try.rb +23 -23
  137. data/try/integration/cross_component_try.rb +1 -1
  138. data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
  139. data/try/integration/data_types/datatype_transactions_try.rb +247 -0
  140. data/try/{core → integration}/database_consistency_try.rb +11 -8
  141. data/try/{core → integration}/familia_extended_try.rb +1 -1
  142. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  143. data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
  144. data/try/{models → integration/models}/customer_try.rb +1 -1
  145. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  146. data/try/{models → integration/models}/familia_object_try.rb +2 -2
  147. data/try/{core → integration}/persistence_operations_try.rb +163 -11
  148. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  149. data/try/{configuration → integration}/scenarios_try.rb +1 -1
  150. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  151. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  152. data/try/performance/benchmarks_try.rb +2 -2
  153. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  154. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  155. data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
  156. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  157. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  158. data/try/{core → unit/core}/connection_try.rb +1 -1
  159. data/try/{core → unit/core}/errors_try.rb +1 -1
  160. data/try/{core → unit/core}/extensions_try.rb +1 -1
  161. data/try/unit/core/familia_logger_try.rb +110 -0
  162. data/try/{core → unit/core}/familia_try.rb +1 -1
  163. data/try/{core → unit/core}/middleware_try.rb +41 -1
  164. data/try/{core → unit/core}/settings_try.rb +1 -1
  165. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  166. data/try/{core → unit/core}/tools_try.rb +1 -1
  167. data/try/{core → unit/core}/utils_try.rb +17 -14
  168. data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
  169. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  170. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  171. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  172. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  173. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  174. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  175. data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
  176. data/try/{data_types → unit/data_types}/string_try.rb +2 -2
  177. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  178. data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
  179. data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
  180. data/try/{horreum → unit/horreum}/base_try.rb +4 -4
  181. data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
  182. data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
  183. data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
  184. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
  185. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  186. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  187. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  188. data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
  189. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  190. data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
  191. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  192. data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
  193. data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
  194. data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
  195. data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
  196. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  197. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  198. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  199. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  200. metadata +147 -126
  201. data/lib/familia/distinguisher.rb +0 -85
  202. data/lib/familia/refinements/logger_trace.rb +0 -60
  203. data/try/refinements/logger_trace_methods_try.rb +0 -44
  204. /data/try/{debugging → support/debugging}/README.md +0 -0
  205. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  206. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  207. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  208. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  209. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  210. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  211. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  212. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  213. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  214. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  215. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  216. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  217. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  218. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  219. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  220. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  221. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  222. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  223. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  224. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  225. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  226. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  227. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  228. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  229. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  230. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  231. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  232. /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
  233. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  234. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  235. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  236. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  237. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  238. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  239. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  240. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  241. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  242. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  243. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  244. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  245. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  246. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  247. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  248. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  249. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -1,6 +1,6 @@
1
1
  # lib/familia/connection/middleware.rb
2
2
 
3
- require_relative '../../middleware/database_middleware'
3
+ require_relative '../../middleware/database_logger'
4
4
 
5
5
  module Familia
6
6
  module Connection
@@ -19,13 +19,13 @@ module Familia
19
19
  # Increments the middleware version, invalidating all cached connections
20
20
  def increment_middleware_version!
21
21
  @middleware_version += 1
22
- Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{@middleware_version}" if Familia.debug?
22
+ Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{@middleware_version}"
23
23
  end
24
24
 
25
25
  # Sets a versioned fiber-local connection
26
- def set_fiber_connection(connection)
26
+ def fiber_connection=(connection)
27
27
  Fiber[:familia_connection] = [connection, middleware_version]
28
- Familia.trace :FIBER_CONNECTION, nil, "Set with version #{middleware_version}" if Familia.debug?
28
+ Familia.trace :FIBER_CONNECTION, nil, "Set with version #{middleware_version}"
29
29
  end
30
30
 
31
31
  # Clears the fiber-local connection
@@ -50,24 +50,78 @@ module Familia
50
50
  increment_middleware_version! if value
51
51
  end
52
52
 
53
+ # Reconnects with fresh middleware registration
54
+ #
55
+ # This method is useful when middleware needs to be applied to connection pools
56
+ # that were created before middleware was enabled. It:
57
+ #
58
+ # 1. Clears the middleware registration flag to allow re-registration
59
+ # 2. Re-runs the middleware registration logic
60
+ # 3. Clears connection chain to force rebuild
61
+ # 4. Increments middleware version to invalidate cached connections
62
+ # 5. Clears fiber-local connections
63
+ #
64
+ # The next connection request will use the updated middleware configuration.
65
+ # Existing connection pools will naturally create new connections with middleware
66
+ # as old connections are cycled out.
67
+ #
68
+ # @note If no middleware is enabled, this method safely clears connection state
69
+ # but won't register any middleware until it's enabled.
70
+ #
71
+ # @example Enable middleware and reconnect
72
+ # Familia.enable_database_logging = true
73
+ # Familia.reconnect!
74
+ #
75
+ # @example In test suites
76
+ # # Test file A creates pools
77
+ # Familia.connection_provider = ->(uri) { pool.with { |c| c } }
78
+ #
79
+ # # Test file B enables middleware
80
+ # Familia.enable_database_logging = true
81
+ # Familia.reconnect! # Force new connections with middleware
82
+ #
83
+ def reconnect!
84
+ # Allow middleware to be re-registered
85
+ @middleware_registered = false
86
+ register_middleware_once
87
+
88
+ # Clear connection chain to force rebuild
89
+ @connection_chain = nil
90
+
91
+ # Increment version to invalidate all cached connections
92
+ increment_middleware_version!
93
+
94
+ # Clear fiber-local connections
95
+ clear_fiber_connection!
96
+
97
+ Familia.trace :RECONNECT, nil, 'Connection chain cleared, will rebuild with current middleware on next use'
98
+ end
99
+
53
100
  private
54
101
 
55
102
  # Registers middleware once globally, regardless of when clients are created.
56
103
  # This prevents duplicate middleware registration and ensures all clients get middleware.
57
104
  def register_middleware_once
105
+ # Skip if already registered
58
106
  return if @middleware_registered
59
107
 
108
+ # Check if any middleware is enabled
109
+ return unless Familia.enable_database_logging || Familia.enable_database_counter
110
+
60
111
  if Familia.enable_database_logging
61
112
  DatabaseLogger.logger = Familia.logger
62
113
  RedisClient.register(DatabaseLogger)
114
+ Familia.trace :MIDDLEWARE_REGISTERED, nil, 'Registered DatabaseLogger'
63
115
  end
64
116
 
65
117
  if Familia.enable_database_counter
66
118
  # NOTE: This middleware uses AtomicFixnum from concurrent-ruby which is
67
119
  # less contentious than Mutex-based counters. Safe for production.
68
120
  RedisClient.register(DatabaseCommandCounter)
121
+ Familia.trace :MIDDLEWARE_REGISTERED, nil, 'Registered DatabaseCommandCounter'
69
122
  end
70
123
 
124
+ # Set flag after successful registration
71
125
  @middleware_registered = true
72
126
  end
73
127
  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
  #
@@ -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
@@ -1,10 +1,10 @@
1
- # lib/familia/data_type/commands.rb
1
+ # lib/familia/data_type/database_commands.rb
2
2
 
3
3
  module Familia
4
4
  class DataType
5
5
  # Must be included in all DataType classes to provide Valkey/Redis
6
6
  # commands. The class must have a dbkey method.
7
- module Commands
7
+ module DatabaseCommands
8
8
  def move(logical_database)
9
9
  dbclient.move dbkey, logical_database
10
10
  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
 
@@ -11,9 +11,9 @@ module Familia
11
11
  # @return [String, nil] The serialized representation of the value, or nil
12
12
  # if serialization fails.
13
13
  #
14
- # @note When a class option is specified, it uses that class's
15
- # serialization method. Otherwise, it relies on Familia.distinguisher for
16
- # serialization.
14
+ # @note When a class option is specified, it uses Familia.identifier_extractor
15
+ # to extract the identifier from objects. Otherwise, it extracts identifiers
16
+ # from Familia::Base instances or class names.
17
17
  #
18
18
  # @example With a class option
19
19
  # serialize_value(User.new(name: "Cloe"), strict_values: false) #=> '{"name":"Cloe"}'
@@ -31,13 +31,13 @@ module Familia
31
31
  Familia.trace :TOREDIS, nil, "#{val}<#{val.class}|#{opts[:class]}>" if Familia.debug?
32
32
 
33
33
  if opts[:class]
34
- prepared = Familia.distinguisher(opts[:class], strict_values: strict_values)
34
+ prepared = Familia.identifier_extractor(opts[:class])
35
35
  Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
36
36
  end
37
37
 
38
38
  if prepared.nil?
39
39
  # Enforce strict values when no class option is specified
40
- prepared = Familia.distinguisher(val, strict_values: true)
40
+ prepared = Familia.identifier_extractor(val)
41
41
  Familia.ld " from <#{val.class}> => <#{prepared.class}>"
42
42
  end
43
43
 
@@ -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
@@ -3,7 +3,7 @@
3
3
  require_relative 'data_type/class_methods'
4
4
  require_relative 'data_type/settings'
5
5
  require_relative 'data_type/connection'
6
- require_relative 'data_type/commands'
6
+ require_relative 'data_type/database_commands'
7
7
  require_relative 'data_type/serialization'
8
8
 
9
9
  # Familia
@@ -77,7 +77,7 @@ module Familia
77
77
 
78
78
  include Settings
79
79
  include Connection
80
- include Commands
80
+ include DatabaseCommands
81
81
  include Serialization
82
82
  end
83
83
 
@@ -44,8 +44,18 @@ module Familia
44
44
  new(**parsed)
45
45
  end
46
46
 
47
- def self.from_json(json_string)
48
- validate!(json_string)
47
+ def self.from_json(json_string_or_hash)
48
+ # Support both JSON strings (legacy) and already-parsed Hashes (v2.0 deserialization)
49
+ if json_string_or_hash.is_a?(Hash)
50
+ # Already parsed - use directly
51
+ parsed = json_string_or_hash
52
+ # Symbolize keys if they're strings
53
+ parsed = parsed.transform_keys(&:to_sym) if parsed.keys.first.is_a?(String)
54
+ new(**parsed)
55
+ else
56
+ # JSON string - validate and parse
57
+ validate!(json_string_or_hash)
58
+ end
49
59
  end
50
60
 
51
61
  # Instance methods for decryptability validation
@@ -32,15 +32,22 @@ module Familia
32
32
  Familia::Encryption.secure_wipe(key) if key
33
33
  end
34
34
 
35
- def decrypt(encrypted_json, context:, additional_data: nil)
36
- return nil if encrypted_json.nil? || encrypted_json.empty?
35
+ def decrypt(encrypted_json_or_hash, context:, additional_data: nil)
36
+ return nil if encrypted_json_or_hash.nil? || (encrypted_json_or_hash.respond_to?(:empty?) && encrypted_json_or_hash.empty?)
37
37
 
38
38
  # Increment counter immediately to track all decryption attempts, even failed ones
39
39
  Familia::Encryption.derivation_count.increment
40
40
 
41
41
  begin
42
- data = Familia::Encryption::EncryptedData.new(**Familia::JsonSerializer.parse(encrypted_json,
43
- symbolize_names: true))
42
+ # Delegate parsing and instantiation to EncryptedData.from_json
43
+ # Wrap validation errors for security (don't expose internal structure details)
44
+ begin
45
+ data = Familia::Encryption::EncryptedData.from_json(encrypted_json_or_hash)
46
+ raise EncryptionError, 'Failed to parse encrypted data' unless data
47
+ rescue EncryptionError => e
48
+ # Re-wrap validation errors with generic message for security
49
+ raise EncryptionError, "Decryption failed: #{e.message}"
50
+ end
44
51
 
45
52
  # Validate algorithm support
46
53
  provider = Registry.get(data.algorithm)
@@ -1,19 +1,52 @@
1
1
  # lib/familia/errors.rb
2
2
  #
3
3
  module Familia
4
+ # Base exception class for all Familia errors
4
5
  class Problem < RuntimeError; end
5
- class NoIdentifier < Problem; end
6
- class NonUniqueKey < Problem; end
7
6
 
8
- class FieldTypeError < Problem; end
9
- class AutoloadError < Problem; end
7
+ # Base exception class for Redis/persistence-related errors
8
+ class PersistenceError < Problem; end
10
9
 
11
- class SerializerError < Problem; end
10
+ # Base exception class for Horreum models
11
+ class HorreumError < Problem; end
12
12
 
13
- # Raised when attempting to start transactions or pipelines on connection types that don't support them
14
- class OperationModeError < Problem; end
13
+ # Raised when an object creation fails (e.g. the identifier
14
+ # is already in use)
15
+ class CreationError < HorreumError; end
15
16
 
16
- class NotDistinguishableError < Problem
17
+ # Raised when an object lacks a required identifier
18
+ class NoIdentifier < HorreumError; end
19
+
20
+ # Raised when a key is expected to be unique but isn't
21
+ class NonUniqueKey < PersistenceError; end
22
+
23
+ # Raised when watch failed (e.g. key was modified), typically
24
+ # retry
25
+ class OptimisticLockError < PersistenceError; end
26
+
27
+ # Raised when a field type is invalid or unexpected
28
+ class FieldTypeError < HorreumError; end
29
+
30
+ # Raised when autoloading fails
31
+ class AutoloadError < HorreumError; end
32
+
33
+ # Raised when serialization or deserialization fails
34
+ class SerializerError < HorreumError; end
35
+
36
+ # Raised when attempting to start transactions or pipelines on
37
+ # connection types that don't support them
38
+ class OperationModeError < PersistenceError; end
39
+
40
+ # Raised when attempting to call a major method like save when
41
+ # inside a transaction or pipeline
42
+ class NestedTransactionError < OperationModeError; end
43
+
44
+ # Raised when attempting to reference a field that doesn't exist
45
+ class UnknownFieldError < HorreumError; end
46
+
47
+ # Raised when a value cannot be converted to a distinguishable
48
+ # string representation
49
+ class NotDistinguishableError < HorreumError
17
50
  attr_reader :value
18
51
 
19
52
  def initialize(value)
@@ -26,7 +59,8 @@ module Familia
26
59
  end
27
60
  end
28
61
 
29
- class NotConnected < Problem
62
+ # Raised when no connection is available for a given URI
63
+ class NotConnected < PersistenceError
30
64
  attr_reader :uri
31
65
 
32
66
  def initialize(uri)
@@ -39,13 +73,15 @@ module Familia
39
73
  end
40
74
  end
41
75
 
42
- # UnsortedSet Familia.connection_provider or use middleware to provide connections.
43
- class NoConnectionAvailable < Problem; end
76
+ # UnsortedSet Familia.connection_provider or use middleware
77
+ # to provide connections.
78
+ class NoConnectionAvailable < PersistenceError; end
44
79
 
45
80
  # Raised when a load method fails to find the requested object
46
- class NotFound < Problem; end
81
+ class NotFound < PersistenceError; end
47
82
 
48
- # Raised when attempting to refresh an object whose key doesn't exist in the database
83
+ # Raised when attempting to refresh an object whose key
84
+ # doesn't exist in the database
49
85
  class KeyNotFoundError < NonUniqueKey
50
86
  attr_reader :key
51
87
 
@@ -59,7 +95,8 @@ module Familia
59
95
  end
60
96
  end
61
97
 
62
- # Raised when attempting to create an object that already exists in the database
98
+ # Raised when attempting to create an object that already
99
+ # exists in the database
63
100
  class RecordExistsError < NonUniqueKey
64
101
  attr_reader :key
65
102
 
@@ -28,7 +28,9 @@ module Familia::Features
28
28
  ]
29
29
 
30
30
  # Ensure the Features module exists within the base module
31
- base.const_set(:Features, Module.new) unless base.const_defined?(:Features) || config_name.eql?('features')
31
+ unless base.const_defined?(:Features) || config_name.eql?('features')
32
+ base.const_set(:Features, Module.new)
33
+ end
32
34
 
33
35
  # Use the shared autoload_files method
34
36
  autoload_files(dir_patterns, log_prefix: "Autoloader[#{config_name}]")