familia 2.0.0.pre18 → 2.0.0.pre21

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 (370) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +4 -9
  3. data/.github/workflows/code-smells.yml +64 -3
  4. data/.pre-commit-config.yaml +8 -6
  5. data/.reek.yml +10 -9
  6. data/.rubocop.yml +4 -0
  7. data/CHANGELOG.rst +205 -88
  8. data/CLAUDE.md +62 -10
  9. data/Gemfile +3 -3
  10. data/Gemfile.lock +27 -62
  11. data/README.md +39 -0
  12. data/bin/try +16 -0
  13. data/bin/tryouts +16 -0
  14. data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
  15. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
  16. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
  17. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
  18. data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
  19. data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
  20. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  21. data/docs/guides/encryption.md +486 -0
  22. data/docs/guides/feature-encrypted-fields.md +123 -7
  23. data/docs/guides/feature-expiration.md +177 -133
  24. data/docs/guides/feature-external-identifiers.md +415 -443
  25. data/docs/guides/feature-object-identifiers.md +400 -269
  26. data/docs/guides/feature-quantization.md +120 -6
  27. data/docs/guides/feature-relationships-indexing.md +318 -0
  28. data/docs/guides/feature-relationships-methods.md +146 -604
  29. data/docs/guides/feature-relationships-participation.md +263 -0
  30. data/docs/guides/feature-relationships.md +118 -136
  31. data/docs/guides/feature-system-devs.md +176 -693
  32. data/docs/guides/feature-system.md +119 -6
  33. data/docs/guides/feature-transient-fields.md +81 -0
  34. data/docs/guides/field-system.md +778 -0
  35. data/docs/guides/index.md +32 -15
  36. data/docs/guides/logging.md +187 -0
  37. data/docs/guides/optimized-loading.md +674 -0
  38. data/docs/guides/thread-safety-monitoring.md +61 -0
  39. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  40. data/docs/migrating/v2.0.0-pre19.md +197 -0
  41. data/docs/migrating/v2.0.0-pre22.md +241 -0
  42. data/docs/overview.md +7 -9
  43. data/docs/reference/api-technical.md +267 -320
  44. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  45. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  46. data/examples/autoloader/mega_customer.rb +2 -0
  47. data/examples/datatype_standalone.rb +282 -0
  48. data/examples/encrypted_fields.rb +2 -1
  49. data/examples/json_usage_patterns.rb +2 -0
  50. data/examples/relationships.rb +3 -0
  51. data/examples/safe_dump.rb +2 -1
  52. data/examples/sampling_demo.rb +53 -0
  53. data/examples/single_connection_transaction_confusions.rb +2 -1
  54. data/familia.gemspec +2 -1
  55. data/lib/familia/base.rb +2 -0
  56. data/lib/familia/connection/behavior.rb +254 -0
  57. data/lib/familia/connection/handlers.rb +97 -0
  58. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  59. data/lib/familia/connection/middleware.rb +34 -24
  60. data/lib/familia/connection/operation_core.rb +3 -1
  61. data/lib/familia/connection/operations.rb +2 -0
  62. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
  63. data/lib/familia/connection/transaction_core.rb +75 -9
  64. data/lib/familia/connection.rb +21 -5
  65. data/lib/familia/data_type/class_methods.rb +3 -1
  66. data/lib/familia/data_type/connection.rb +153 -7
  67. data/lib/familia/data_type/database_commands.rb +9 -4
  68. data/lib/familia/data_type/serialization.rb +10 -4
  69. data/lib/familia/data_type/settings.rb +2 -0
  70. data/lib/familia/data_type/types/counter.rb +2 -0
  71. data/lib/familia/data_type/types/hashkey.rb +8 -6
  72. data/lib/familia/data_type/types/listkey.rb +2 -0
  73. data/lib/familia/data_type/types/lock.rb +2 -0
  74. data/lib/familia/data_type/types/sorted_set.rb +2 -0
  75. data/lib/familia/data_type/types/stringkey.rb +2 -0
  76. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  77. data/lib/familia/data_type.rb +2 -0
  78. data/lib/familia/encryption/encrypted_data.rb +4 -2
  79. data/lib/familia/encryption/manager.rb +2 -0
  80. data/lib/familia/encryption/provider.rb +2 -0
  81. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  82. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  83. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  84. data/lib/familia/encryption/registry.rb +2 -0
  85. data/lib/familia/encryption/request_cache.rb +2 -0
  86. data/lib/familia/encryption.rb +9 -2
  87. data/lib/familia/errors.rb +53 -14
  88. data/lib/familia/features/autoloader.rb +2 -0
  89. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  90. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  91. data/lib/familia/features/encrypted_fields.rb +2 -2
  92. data/lib/familia/features/expiration/extensions.rb +11 -11
  93. data/lib/familia/features/expiration.rb +29 -21
  94. data/lib/familia/features/external_identifier.rb +33 -7
  95. data/lib/familia/features/object_identifier.rb +2 -0
  96. data/lib/familia/features/quantization.rb +3 -1
  97. data/lib/familia/features/relationships/README.md +3 -1
  98. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  99. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
  100. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  101. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
  102. data/lib/familia/features/relationships/indexing.rb +40 -42
  103. data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
  104. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  105. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  106. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  107. data/lib/familia/features/relationships/participation.rb +155 -69
  108. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  109. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  110. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  111. data/lib/familia/features/relationships.rb +5 -3
  112. data/lib/familia/features/safe_dump.rb +2 -0
  113. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  114. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  115. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  116. data/lib/familia/features/transient_fields.rb +2 -0
  117. data/lib/familia/features.rb +2 -0
  118. data/lib/familia/field_type.rb +5 -2
  119. data/lib/familia/horreum/connection.rb +28 -36
  120. data/lib/familia/horreum/database_commands.rb +131 -10
  121. data/lib/familia/horreum/definition.rb +18 -7
  122. data/lib/familia/horreum/management.rb +233 -57
  123. data/lib/familia/horreum/persistence.rb +314 -122
  124. data/lib/familia/horreum/related_fields.rb +2 -0
  125. data/lib/familia/horreum/serialization.rb +26 -4
  126. data/lib/familia/horreum/settings.rb +2 -0
  127. data/lib/familia/horreum/utils.rb +2 -8
  128. data/lib/familia/horreum.rb +46 -13
  129. data/lib/familia/identifier_extractor.rb +2 -0
  130. data/lib/familia/instrumentation.rb +156 -0
  131. data/lib/familia/json_serializer.rb +2 -0
  132. data/lib/familia/logging.rb +94 -37
  133. data/lib/familia/refinements/dear_json.rb +2 -0
  134. data/lib/familia/refinements/stylize_words.rb +2 -14
  135. data/lib/familia/refinements/time_literals.rb +2 -0
  136. data/lib/familia/refinements.rb +2 -0
  137. data/lib/familia/secure_identifier.rb +10 -2
  138. data/lib/familia/settings.rb +9 -7
  139. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  140. data/lib/familia/thread_safety/monitor.rb +328 -0
  141. data/lib/familia/utils.rb +13 -0
  142. data/lib/familia/verifiable_identifier.rb +3 -1
  143. data/lib/familia/version.rb +3 -1
  144. data/lib/familia.rb +31 -4
  145. data/lib/middleware/database_command_counter.rb +152 -0
  146. data/lib/middleware/database_logger.rb +325 -129
  147. data/lib/multi_result.rb +2 -0
  148. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  149. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  150. data/try/edge_cases/json_serialization_try.rb +2 -0
  151. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  152. data/try/edge_cases/race_conditions_try.rb +4 -0
  153. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  154. data/try/edge_cases/string_coercion_try.rb +6 -4
  155. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  156. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  157. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  158. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  159. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  160. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  161. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  162. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  163. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  164. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  165. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  166. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  167. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  168. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  169. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  170. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  171. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  172. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  173. data/try/features/encryption/config_persistence_try.rb +4 -0
  174. data/try/features/encryption/core_try.rb +4 -0
  175. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  176. data/try/features/encryption/module_loading_try.rb +4 -0
  177. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  178. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  179. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  180. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  181. data/try/features/expiration/expiration_try.rb +5 -1
  182. data/try/features/external_identifier/external_identifier_try.rb +171 -8
  183. data/try/features/feature_dependencies_try.rb +2 -0
  184. data/try/features/feature_improvements_try.rb +2 -0
  185. data/try/features/field_groups_try.rb +2 -0
  186. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  187. data/try/features/object_identifier/object_identifier_try.rb +2 -0
  188. data/try/features/quantization/quantization_try.rb +4 -0
  189. data/try/features/real_feature_integration_try.rb +2 -0
  190. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  191. data/try/features/relationships/indexing_rebuild_try.rb +600 -0
  192. data/try/features/relationships/indexing_try.rb +30 -4
  193. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  194. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  195. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  196. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  197. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  198. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  199. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  200. data/try/features/relationships/relationships_api_changes_try.rb +6 -4
  201. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  202. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  203. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  204. data/try/features/relationships/relationships_performance_try.rb +4 -0
  205. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  206. data/try/features/relationships/relationships_try.rb +6 -4
  207. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  208. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  209. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  210. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  211. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  212. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  213. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  214. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  215. data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
  216. data/try/integration/connection/handler_constraints_try.rb +4 -0
  217. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  218. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  219. data/try/integration/connection/operation_mode_guards_try.rb +5 -1
  220. data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
  221. data/try/integration/connection/pools_try.rb +4 -0
  222. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  223. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  224. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  225. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  226. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  227. data/try/integration/connection/transaction_modes_try.rb +4 -0
  228. data/try/integration/conventional_inheritance_try.rb +4 -0
  229. data/try/integration/create_method_try.rb +26 -22
  230. data/try/integration/cross_component_try.rb +4 -0
  231. data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
  232. data/try/integration/data_types/datatype_transactions_try.rb +251 -0
  233. data/try/integration/database_consistency_try.rb +4 -0
  234. data/try/integration/familia_extended_try.rb +4 -0
  235. data/try/integration/familia_members_methods_try.rb +4 -0
  236. data/try/integration/models/customer_safe_dump_try.rb +9 -1
  237. data/try/integration/models/customer_try.rb +4 -0
  238. data/try/integration/models/datatype_base_try.rb +4 -0
  239. data/try/integration/models/familia_object_try.rb +5 -1
  240. data/try/integration/persistence_operations_try.rb +166 -10
  241. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  242. data/try/integration/save_methods_consistency_try.rb +241 -0
  243. data/try/integration/scenarios_try.rb +4 -0
  244. data/try/integration/secure_identifier_try.rb +4 -0
  245. data/try/integration/transaction_safety_core_try.rb +176 -0
  246. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  247. data/try/integration/verifiable_identifier_try.rb +4 -0
  248. data/try/investigation/pipeline_routing/README.md +228 -0
  249. data/try/performance/benchmarks_try.rb +4 -0
  250. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  251. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  252. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  253. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  254. data/try/support/debugging/debug_aad_process.rb +3 -0
  255. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  256. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  257. data/try/support/debugging/debug_context_aad.rb +3 -0
  258. data/try/support/debugging/debug_context_simple.rb +3 -0
  259. data/try/support/debugging/debug_cross_context.rb +3 -0
  260. data/try/support/debugging/debug_database_load.rb +3 -0
  261. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  262. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  263. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  264. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  265. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  266. data/try/support/debugging/debug_load_path.rb +3 -0
  267. data/try/support/debugging/debug_method_definition.rb +3 -0
  268. data/try/support/debugging/debug_method_resolution.rb +3 -0
  269. data/try/support/debugging/debug_minimal.rb +3 -0
  270. data/try/support/debugging/debug_provider.rb +3 -0
  271. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  272. data/try/support/debugging/debug_string_class.rb +3 -0
  273. data/try/support/debugging/debug_test.rb +3 -0
  274. data/try/support/debugging/debug_test_design.rb +3 -0
  275. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  276. data/try/support/debugging/provider_diagnostics.rb +4 -0
  277. data/try/support/helpers/test_cleanup.rb +4 -0
  278. data/try/support/helpers/test_helpers.rb +5 -0
  279. data/try/support/memory/memory_basic_test.rb +4 -0
  280. data/try/support/memory/memory_detailed_test.rb +4 -0
  281. data/try/support/memory/memory_search_for_string.rb +4 -0
  282. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  283. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  284. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  285. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  286. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  287. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  288. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  289. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  290. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  291. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  292. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  293. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  294. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  295. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  296. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  297. data/try/thread_safety/README.md +496 -0
  298. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  299. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  300. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  301. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  302. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  303. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  304. data/try/thread_safety/field_registration_race_try.rb +222 -0
  305. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  306. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  307. data/try/thread_safety/module_config_race_try.rb +175 -0
  308. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  309. data/try/unit/core/autoloader_try.rb +4 -0
  310. data/try/unit/core/base_enhancements_try.rb +4 -0
  311. data/try/unit/core/connection_try.rb +4 -0
  312. data/try/unit/core/errors_try.rb +4 -0
  313. data/try/unit/core/extensions_try.rb +4 -0
  314. data/try/unit/core/familia_logger_try.rb +2 -0
  315. data/try/unit/core/familia_try.rb +4 -0
  316. data/try/unit/core/middleware_sampling_try.rb +335 -0
  317. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  318. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  319. data/try/unit/core/middleware_try.rb +4 -0
  320. data/try/unit/core/settings_try.rb +4 -0
  321. data/try/unit/core/time_utils_try.rb +4 -0
  322. data/try/unit/core/tools_try.rb +4 -0
  323. data/try/unit/core/utils_try.rb +37 -0
  324. data/try/unit/data_types/boolean_try.rb +5 -1
  325. data/try/unit/data_types/counter_try.rb +4 -0
  326. data/try/unit/data_types/datatype_base_try.rb +4 -0
  327. data/try/unit/data_types/hash_try.rb +4 -0
  328. data/try/unit/data_types/list_try.rb +4 -0
  329. data/try/unit/data_types/lock_try.rb +4 -0
  330. data/try/unit/data_types/sorted_set_try.rb +4 -0
  331. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  332. data/try/unit/data_types/string_try.rb +5 -1
  333. data/try/unit/data_types/unsortedset_try.rb +4 -0
  334. data/try/unit/familia_resolve_class_try.rb +116 -0
  335. data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
  336. data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
  337. data/try/unit/horreum/base_try.rb +5 -1
  338. data/try/unit/horreum/class_methods_try.rb +6 -2
  339. data/try/unit/horreum/commands_try.rb +4 -0
  340. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  341. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
  342. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  343. data/try/unit/horreum/field_categories_try.rb +4 -0
  344. data/try/unit/horreum/field_definition_try.rb +4 -0
  345. data/try/unit/horreum/initialization_try.rb +5 -1
  346. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  347. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  348. data/try/unit/horreum/relations_try.rb +8 -4
  349. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  350. data/try/unit/horreum/serialization_try.rb +6 -2
  351. data/try/unit/horreum/settings_try.rb +4 -0
  352. data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
  353. data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
  354. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  355. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  356. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  357. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  358. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  359. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  360. data/try/unit/thread_safety_monitor_try.rb +149 -0
  361. metadata +81 -14
  362. data/.github/workflows/code-quality.yml +0 -138
  363. data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
  364. data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
  365. data/docs/archive/FAMILIA_UPDATE.md +0 -226
  366. data/docs/archive/README.md +0 -64
  367. data/docs/archive/api-reference.md +0 -333
  368. data/docs/guides/core-field-system.md +0 -806
  369. data/docs/guides/implementation.md +0 -276
  370. data/docs/guides/security-model.md +0 -183
@@ -1,4 +1,6 @@
1
1
  # lib/familia/connection/handlers.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # Familia
4
6
  #
@@ -219,5 +221,100 @@ module Familia
219
221
  dbclient
220
222
  end
221
223
  end
224
+
225
+ # Handler for delegating connection resolution to parent object
226
+ #
227
+ # Used by DataType objects that are attached to a parent (Horreum instance or class).
228
+ # Delegates the connection resolution to the parent's dbclient method, which allows
229
+ # DataType objects to inherit connection settings, logical_database, and transaction
230
+ # context from their parent.
231
+ #
232
+ # This preserves the existing architectural pattern where DataType objects owned by
233
+ # Horreum models use the parent's connection chain. This is the primary behavior
234
+ # for DataType objects in typical usage.
235
+ #
236
+ # @example Instance-level DataType with parent
237
+ # user = User.new(userid: 'user_123')
238
+ # user.tags # DataType that delegates to user.dbclient
239
+ #
240
+ # @example Class-level DataType with parent
241
+ # User.global_users # DataType that delegates to User.dbclient
242
+ #
243
+ class ParentDelegationHandler < BaseConnectionHandler
244
+ @allows_transaction = true
245
+ @allows_pipelined = true
246
+
247
+ def initialize(data_type)
248
+ @data_type = data_type
249
+ end
250
+
251
+ def handle(uri)
252
+ return nil unless @data_type.parent
253
+
254
+ # Delegate to parent's connection chain
255
+ # Parent can be either a Horreum class or instance
256
+ parent_connection = @data_type.parent.dbclient(uri)
257
+
258
+ if parent_connection
259
+ Familia.trace :DBCLIENT_PARENT_DELEGATION, @data_type.dbkey,
260
+ "Using parent connection from #{@data_type.parent.class}"
261
+ end
262
+
263
+ parent_connection
264
+ end
265
+ end
266
+
267
+ # Handler for standalone DataType objects without a parent
268
+ #
269
+ # Provides connection resolution for DataType objects that are created independently
270
+ # rather than being attached to a Horreum model. Checks for instance-level @dbclient
271
+ # first, then falls back to creating a connection based on logical_database option
272
+ # or global Familia connection.
273
+ #
274
+ # This enables standalone DataType usage patterns like Rack::Session implementations
275
+ # where DataType objects need independent connection management and transaction support.
276
+ #
277
+ # @example Standalone DataType with custom connection
278
+ # leaderboard = Familia::SortedSet.new('game:leaderboard')
279
+ # leaderboard.dbclient = ConnectionPool.new { Redis.new }
280
+ #
281
+ # @example Standalone DataType with logical_database option
282
+ # cache = Familia::HashKey.new('app:cache', logical_database: 2)
283
+ #
284
+ class StandaloneConnectionHandler < BaseConnectionHandler
285
+ @allows_transaction = true
286
+ @allows_pipelined = true
287
+
288
+ def initialize(data_type)
289
+ @data_type = data_type
290
+ end
291
+
292
+ def handle(uri)
293
+ # If a specific URI is provided, always use it to get a connection.
294
+ if uri
295
+ connection = Familia.dbclient(uri)
296
+ Familia.trace :DBCLIENT_STANDALONE_DATATYPE, @data_type.dbkey,
297
+ "Created standalone connection for specific URI: #{uri}"
298
+ return connection
299
+ end
300
+
301
+ # Use instance @dbclient if explicitly set and no URI was passed
302
+ instance_dbclient = @data_type.instance_variable_get(:@dbclient)
303
+ if instance_dbclient
304
+ Familia.trace :DBCLIENT_DATATYPE_INSTANCE, @data_type.dbkey,
305
+ 'Using DataType instance @dbclient'
306
+ return instance_dbclient
307
+ end
308
+
309
+ # Fall back to creating connection based on opts or global
310
+ target_uri = @data_type.opts[:logical_database]
311
+ connection = Familia.dbclient(target_uri)
312
+
313
+ Familia.trace :DBCLIENT_STANDALONE_DATATYPE, @data_type.dbkey,
314
+ "Created standalone connection for #{target_uri || 'default'}"
315
+
316
+ connection
317
+ end
318
+ end
222
319
  end
223
320
  end
@@ -1,4 +1,6 @@
1
1
  # lib/familia/connection/individual_command_proxy.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Connection
@@ -1,6 +1,9 @@
1
1
  # lib/familia/connection/middleware.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative '../../middleware/database_logger'
6
+ require_relative '../../middleware/database_command_counter'
4
7
 
5
8
  module Familia
6
9
  module Connection
@@ -13,19 +16,20 @@ module Familia
13
16
 
14
17
  # @return [Integer] Current middleware version for cache invalidation
15
18
  def middleware_version
16
- @middleware_version
19
+ @middleware_version.value
17
20
  end
18
21
 
19
22
  # Increments the middleware version, invalidating all cached connections
20
23
  def increment_middleware_version!
21
- @middleware_version += 1
22
- Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{@middleware_version}"
24
+ new_version = @middleware_version.increment
25
+ Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{new_version}"
23
26
  end
24
27
 
25
28
  # Sets a versioned fiber-local connection
26
29
  def fiber_connection=(connection)
27
- Fiber[:familia_connection] = [connection, middleware_version]
28
- Familia.trace :FIBER_CONNECTION, nil, "Set with version #{middleware_version}"
30
+ current_version = middleware_version
31
+ Fiber[:familia_connection] = [connection, current_version]
32
+ Familia.trace :FIBER_CONNECTION, nil, "Set with version #{current_version}"
29
33
  end
30
34
 
31
35
  # Clears the fiber-local connection
@@ -81,18 +85,23 @@ module Familia
81
85
  # Familia.reconnect! # Force new connections with middleware
82
86
  #
83
87
  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!
88
+ # Thread-safe: Use same mutex as dbclient to protect @connection_chain
89
+ @connection_chain_mutex.synchronize do
90
+ # Allow middleware to be re-registered by resetting all flags
91
+ @middleware_registered = false
92
+ @logger_registered = false
93
+ @counter_registered = false
94
+ register_middleware_once
95
+
96
+ # Clear connection chain to force rebuild
97
+ @connection_chain = nil
98
+
99
+ # Increment version to invalidate all cached connections
100
+ increment_middleware_version!
101
+
102
+ # Clear fiber-local connections
103
+ clear_fiber_connection!
104
+ end
96
105
 
97
106
  Familia.trace :RECONNECT, nil, 'Connection chain cleared, will rebuild with current middleware on next use'
98
107
  end
@@ -102,27 +111,28 @@ module Familia
102
111
  # Registers middleware once globally, regardless of when clients are created.
103
112
  # This prevents duplicate middleware registration and ensures all clients get middleware.
104
113
  def register_middleware_once
105
- # Skip if already registered
106
- return if @middleware_registered
107
-
108
114
  # Check if any middleware is enabled
109
115
  return unless Familia.enable_database_logging || Familia.enable_database_counter
110
116
 
111
- if Familia.enable_database_logging
117
+ # Register each middleware independently to avoid early return bug
118
+ # where enabling one middleware prevents the other from being registered
119
+ if Familia.enable_database_logging && !@logger_registered
112
120
  DatabaseLogger.logger = Familia.logger
113
121
  RedisClient.register(DatabaseLogger)
114
122
  Familia.trace :MIDDLEWARE_REGISTERED, nil, 'Registered DatabaseLogger'
123
+ @logger_registered = true
115
124
  end
116
125
 
117
- if Familia.enable_database_counter
126
+ if Familia.enable_database_counter && !@counter_registered
118
127
  # NOTE: This middleware uses AtomicFixnum from concurrent-ruby which is
119
128
  # less contentious than Mutex-based counters. Safe for production.
120
129
  RedisClient.register(DatabaseCommandCounter)
121
130
  Familia.trace :MIDDLEWARE_REGISTERED, nil, 'Registered DatabaseCommandCounter'
131
+ @counter_registered = true
122
132
  end
123
133
 
124
- # Set flag after successful registration
125
- @middleware_registered = true
134
+ # Set global flag when any middleware is registered
135
+ @middleware_registered = @logger_registered || @counter_registered
126
136
  end
127
137
  end
128
138
  end
@@ -1,3 +1,5 @@
1
+ # lib/familia/connection/operation_core.rb
2
+ #
1
3
  # frozen_string_literal: true
2
4
 
3
5
  module Familia
@@ -45,7 +47,7 @@ module Familia
45
47
  when :transaction
46
48
  Familia.transaction_mode
47
49
  when :pipeline
48
- Familia.pipeline_mode
50
+ Familia.pipelined_mode
49
51
  else
50
52
  :strict
51
53
  end
@@ -1,4 +1,6 @@
1
1
  # lib/familia/connection/operations.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # Familia
4
6
  #
@@ -1,3 +1,5 @@
1
+ # lib/familia/connection/pipelined_core.rb
2
+ #
1
3
  # frozen_string_literal: true
2
4
 
3
5
  module Familia
@@ -16,7 +18,7 @@ module Familia
16
18
  #
17
19
  # Handles pipeline execution based on connection handler capabilities.
18
20
  # When handler doesn't support pipelines, fallback behavior is controlled
19
- # by Familia.pipeline_mode setting.
21
+ # by Familia.pipelined_mode setting.
20
22
  #
21
23
  # @param dbclient_proc [Proc] Lambda that returns the Redis connection
22
24
  # @param block [Proc] Block containing Redis commands to execute
@@ -32,7 +34,7 @@ module Familia
32
34
  # result.results # => ["OK", 1]
33
35
  #
34
36
  # @example With fallback modes
35
- # Familia.configure { |c| c.pipeline_mode = :permissive }
37
+ # Familia.configure { |c| c.pipelined_mode = :permissive }
36
38
  # result = PipelineCore.execute_pipeline(-> { cached_conn }) do |conn|
37
39
  # conn.set('key', 'value') # Executes individually, no error
38
40
  # end
@@ -1,4 +1,6 @@
1
1
  # lib/familia/connection/transaction_core.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Connection
@@ -8,11 +10,62 @@ module Familia
8
10
  # behavior when transactions are unavailable due to connection handler constraints.
9
11
  # Eliminates code duplication between Operations and Horreum Connection modules.
10
12
  #
13
+ # ## Transaction Safety Rules
14
+ #
15
+ # ### Rule 1: No Save Operations Inside Transactions
16
+ # The following methods CANNOT be called within a transaction context:
17
+ # - `save`, `save!`, `save_if_not_exists!`, `create!`
18
+ # These methods require reading current state for validation, which would
19
+ # return uninspectable Redis::Future objects inside transactions.
20
+ #
21
+ # ### Rule 2: Reentrant Transaction Behavior
22
+ # Nested transaction calls reuse the same connection and do not create new
23
+ # MULTI/EXEC blocks. This ensures atomicity across nested operations.
24
+ #
25
+ # ### Rule 3: Read Operations Return Futures
26
+ # Inside transactions, read operations return Redis::Future objects that
27
+ # cannot be inspected until the transaction completes. Always check conditions
28
+ # before entering the transaction.
29
+ #
30
+ # ### Rule 4: Connection Handler Compatibility
31
+ # - **FiberTransactionHandler**: Supports reentrant transactions
32
+ # - **ProviderConnectionHandler**: Full transaction support
33
+ # - **CreateConnectionHandler**: Full transaction support
34
+ # - **FiberConnectionHandler**: Blocked (raises OperationModeError)
35
+ # - **DefaultConnectionHandler**: Blocked (raises OperationModeError)
36
+ #
37
+ # @example Correct Pattern: Save Before Transaction
38
+ # customer = Customer.new(email: 'test@example.com')
39
+ # customer.save # Validates unique constraints here
40
+ #
41
+ # customer.transaction do
42
+ # customer.increment(:login_count)
43
+ # customer.hset(:last_login, Time.now.to_i)
44
+ # end
45
+ #
46
+ # @example Incorrect Pattern: Save Inside Transaction
47
+ # Customer.transaction do
48
+ # customer = Customer.new(email: 'test@example.com')
49
+ # customer.save # Raises Familia::OperationModeError
50
+ # end
51
+ #
52
+ # @example Reentrant Transactions
53
+ # Customer.transaction do |outer_conn|
54
+ # outer_conn.set('key1', 'value1')
55
+ #
56
+ # # Nested call reuses same connection - no new MULTI/EXEC
57
+ # Customer.transaction do |inner_conn|
58
+ # inner_conn.set('key2', 'value2') # Same connection as outer
59
+ # end
60
+ # end
61
+ #
11
62
  # @example Usage in transaction methods
12
63
  # def transaction(&block)
13
64
  # TransactionCore.execute_transaction(-> { dbclient }, &block)
14
65
  # end
15
66
  #
67
+ # @see docs/transaction_safety.md for complete safety guidelines
68
+ #
16
69
  module TransactionCore
17
70
  # Executes a transaction with configurable fallback behavior
18
71
  #
@@ -21,6 +74,11 @@ module Familia
21
74
  # 2. Reentrant transaction when already within a transaction context
22
75
  # 3. Individual command execution with configurable error/warn/silent modes
23
76
  #
77
+ # ## Safety Mechanisms
78
+ # - Fiber-local storage tracks transaction state across nested calls
79
+ # - Connection handler validation prevents unsafe transaction usage
80
+ # - Automatic cleanup ensures proper state management even on exceptions
81
+ #
24
82
  # @param dbclient_proc [Proc] Lambda that returns the Redis connection
25
83
  # @param block [Proc] Block containing Redis commands to execute
26
84
  # @return [MultiResult] Result object with success status and command results
@@ -34,27 +92,25 @@ module Familia
34
92
  # result.successful? # => true/false
35
93
  # result.results # => ["OK", 1]
36
94
  #
37
- def self.execute_transaction(dbclient_proc, &block)
95
+ def self.execute_transaction(dbclient_proc, &)
38
96
  # First, get the connection to populate the handler class
39
- connection = dbclient_proc.call
97
+ dbclient_proc.call
40
98
  handler_class = Fiber[:familia_connection_handler_class]
41
99
 
42
100
  # Check transaction capability
43
101
  transaction_capability = handler_class&.allows_transaction
44
102
 
45
103
  if transaction_capability == false
46
- handle_transaction_fallback(dbclient_proc, handler_class, &block)
104
+ handle_transaction_fallback(dbclient_proc, handler_class, &)
47
105
  elsif transaction_capability == :reentrant
48
106
  # Already in transaction, just yield the connection
49
107
  yield(Fiber[:familia_transaction])
50
108
  else
51
109
  # Normal transaction flow (includes nil, true, and other values)
52
- execute_normal_transaction(dbclient_proc, &block)
110
+ execute_normal_transaction(dbclient_proc, &)
53
111
  end
54
112
  end
55
113
 
56
- private
57
-
58
114
  # Handles transaction fallback based on configured transaction mode
59
115
  #
60
116
  # Delegates to OperationCore.handle_fallback for consistent behavior
@@ -65,8 +121,8 @@ module Familia
65
121
  # @param block [Proc] Block containing Redis commands to execute
66
122
  # @return [MultiResult] Result from individual command execution or raises error
67
123
  #
68
- def self.handle_transaction_fallback(dbclient_proc, handler_class, &block)
69
- OperationCore.handle_fallback(:transaction, dbclient_proc, handler_class, &block)
124
+ def self.handle_transaction_fallback(dbclient_proc, handler_class, &)
125
+ OperationCore.handle_fallback(:transaction, dbclient_proc, handler_class, &)
70
126
  end
71
127
 
72
128
  # Executes a normal Redis transaction using MULTI/EXEC
@@ -74,11 +130,21 @@ module Familia
74
130
  # Handles the standard transaction flow including nested transaction detection,
75
131
  # proper Fiber-local state management, and cleanup in ensure blocks.
76
132
  #
133
+ # ## Implementation Details
134
+ # - Uses Fiber[:familia_transaction] to track active transaction connection
135
+ # - Reentrant behavior: yields existing connection if already in transaction
136
+ # - All commands queued and executed atomically on EXEC
137
+ # - Returns MultiResult with success status and command results
138
+ #
139
+ # ## Thread Safety
140
+ # Each thread has its own root fiber with isolated fiber-local storage,
141
+ # ensuring transactions don't interfere across threads.
142
+ #
77
143
  # @param dbclient_proc [Proc] Lambda that returns the Redis connection
78
144
  # @param block [Proc] Block containing Redis commands to execute
79
145
  # @return [MultiResult] Result object with transaction command results
80
146
  #
81
- def self.execute_normal_transaction(dbclient_proc, &block)
147
+ def self.execute_normal_transaction(dbclient_proc)
82
148
  # Check for existing transaction context
83
149
  return yield(Fiber[:familia_transaction]) if Fiber[:familia_transaction]
84
150
 
@@ -1,12 +1,15 @@
1
1
  # lib/familia/connection.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
5
+ require_relative 'connection/behavior'
3
6
  require_relative 'connection/handlers'
4
7
  require_relative 'connection/middleware'
5
8
  require_relative 'connection/operations'
6
9
  require_relative 'connection/individual_command_proxy'
7
10
  require_relative 'connection/operation_core'
8
11
  require_relative 'connection/transaction_core'
9
- require_relative 'connection/pipeline_core'
12
+ require_relative 'connection/pipelined_core'
10
13
 
11
14
  # Familia
12
15
  #
@@ -15,7 +18,10 @@ require_relative 'connection/pipeline_core'
15
18
  module Familia
16
19
  @uri = URI.parse 'redis://127.0.0.1:6379'
17
20
  @middleware_registered = false
18
- @middleware_version = 0
21
+ @logger_registered = false
22
+ @counter_registered = false
23
+ @middleware_version = Concurrent::AtomicFixnum.new(0)
24
+ @connection_chain_mutex = Familia::ThreadSafety::InstrumentedMutex.new('connection_chain') # Thread-safe connection chain initialization
19
25
 
20
26
  # The Connection module provides Database connection management for Familia.
21
27
  # It allows easy setup and access to Database clients across different URIs
@@ -42,7 +48,7 @@ module Familia
42
48
  @connection_chain = nil # Force rebuild of chain
43
49
  end
44
50
 
45
- # Sets the default URI for Database connections.
51
+ # Sets the default URI for Database connections.
46
52
  #
47
53
  # NOTE: uri is not a property of the Settings module b/c it's not
48
54
  # configured in class defintions like default_expiration or logical DB index.
@@ -85,12 +91,22 @@ module Familia
85
91
  # Retrieves a Database connection using the Chain of Responsibility pattern.
86
92
  # Handles DB selection automatically based on the URI.
87
93
  #
94
+ # Thread-safe: Uses double-checked locking pattern to avoid mutex overhead
95
+ # on the hot path. Only acquires mutex during initial lazy initialization.
96
+ # MRI's GIL provides implicit memory barriers making this pattern safe.
97
+ #
88
98
  # @return [Redis] The Database client for the specified URI
89
99
  # @example Familia.dbclient('redis://localhost:6379/1')
90
100
  # Familia.dbclient(2) # Use DB 2 with default server
91
101
  def dbclient(uri = nil)
92
- @connection_chain ||= build_connection_chain
93
- @connection_chain.handle(uri)
102
+ # Fast path - read with local variable to ensure single read
103
+ chain = @connection_chain
104
+ return chain.handle(uri) if chain
105
+
106
+ # Slow path - initialization only
107
+ @connection_chain_mutex.synchronize do
108
+ @connection_chain ||= build_connection_chain
109
+ end.handle(uri)
94
110
  end
95
111
 
96
112
  # Builds the connection chain with handlers in priority order
@@ -1,4 +1,6 @@
1
- # lib/familia/data_type/definition.rb
1
+ # lib/familia/data_type/class_methods.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class DataType
@@ -1,25 +1,110 @@
1
1
  # lib/familia/data_type/connection.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class DataType
5
7
  # Connection - Instance-level connection and key generation methods
6
8
  #
7
9
  # This module provides instance methods for database connection resolution
8
- # and Redis key generation for DataType objects.
10
+ # and Redis key generation for DataType objects. It includes shared connection
11
+ # behavior from Familia::Connection::Behavior, enabling transaction and pipeline
12
+ # support for both parent-owned and standalone DataType objects.
9
13
  #
10
14
  # Key features:
11
15
  # * Database connection resolution with Chain of Responsibility pattern
12
16
  # * Redis key generation based on parent context
13
17
  # * Direct database access for advanced operations
18
+ # * Transaction support (MULTI/EXEC) for atomic operations
19
+ # * Pipeline support for batched command execution
20
+ # * Parent delegation for owned DataType objects
21
+ # * Standalone connection management for independent DataType objects
22
+ #
23
+ # Connection Chain Priority:
24
+ # 1. FiberTransactionHandler - Active transaction context
25
+ # 2. FiberConnectionHandler - Fiber-local connections
26
+ # 3. ProviderConnectionHandler - User-defined connection provider
27
+ # 4. ParentDelegationHandler - Delegate to parent object (primary for owned DataTypes)
28
+ # 5. StandaloneConnectionHandler - Independent DataType connection
29
+ #
30
+ # @example Parent-owned DataType (automatic delegation)
31
+ # class User < Familia::Horreum
32
+ # logical_database 2
33
+ # zset :scores
34
+ # end
35
+ #
36
+ # user = User.new(userid: 'user_123')
37
+ # user.scores.transaction do |conn|
38
+ # conn.zadd(user.scores.dbkey, 100, 'level1')
39
+ # conn.zadd(user.scores.dbkey, 200, 'level2')
40
+ # end
41
+ #
42
+ # @example Standalone DataType with transaction
43
+ # leaderboard = Familia::SortedSet.new('game:leaderboard')
44
+ # leaderboard.transaction do |conn|
45
+ # conn.zadd(leaderboard.dbkey, 500, 'player1')
46
+ # conn.zadd(leaderboard.dbkey, 600, 'player2')
47
+ # end
14
48
  #
15
49
  module Connection
16
- # TODO: Replace with Chain of Responsibility pattern
17
- def dbclient
50
+ include Familia::Connection::Behavior
51
+
52
+ # Returns the effective URI this DataType will use for connections
53
+ #
54
+ # For parent-owned DataTypes, delegates to parent's URI.
55
+ # For standalone DataTypes with logical_database option, constructs URI with that database.
56
+ # For standalone DataTypes without options, returns global Familia.uri.
57
+ # Explicit @uri assignment (via uri=) takes precedence.
58
+ #
59
+ # @return [URI, nil] The URI for database connections
60
+ #
61
+ def uri
62
+ return @uri if defined?(@uri) && @uri
63
+ return parent.uri if parent && parent.respond_to?(:uri)
64
+
65
+ # Check opts[:logical_database] first, then parent's logical_database
66
+ db_num = opts[:logical_database]
67
+ db_num ||= parent.logical_database if parent && parent.respond_to?(:logical_database)
68
+
69
+ if db_num
70
+ # Create a new URI with the database number but without custom port
71
+ # This ensures consistent URI representation (e.g., redis://host/db not redis://host:port/db)
72
+ base_uri = Familia.uri
73
+ URI.parse("redis://#{base_uri.host}/#{db_num}")
74
+ else
75
+ Familia.uri
76
+ end
77
+ end
78
+
79
+ # Retrieves a Database connection using the Chain of Responsibility pattern
80
+ #
81
+ # Implements connection resolution optimized for DataType usage patterns:
82
+ # - Fast path check for active transaction context
83
+ # - Full connection chain for comprehensive resolution
84
+ # - Parent delegation as primary behavior for owned DataTypes
85
+ # - Standalone connection handling for independent DataTypes
86
+ #
87
+ # Note: We don't cache the connection chain in an instance variable because
88
+ # DataType objects are frozen for thread safety. Building the chain is cheap
89
+ # (just creating handler objects), and the actual connection resolution work
90
+ # is done by the handlers themselves.
91
+ #
92
+ # @param uri [String, URI, Integer, nil] Optional URI for database selection
93
+ # @return [Redis] The Database client for the specified URI
94
+ #
95
+ # @example Getting connection from parent-owned DataType
96
+ # user.tags.dbclient # Delegates to user.dbclient
97
+ #
98
+ # @example Getting connection from standalone DataType
99
+ # cache = Familia::HashKey.new('app:cache', logical_database: 2)
100
+ # cache.dbclient # Uses standalone handler with db 2
101
+ #
102
+ def dbclient(uri = nil)
103
+ # Fast path for transaction context (highest priority)
18
104
  return Fiber[:familia_transaction] if Fiber[:familia_transaction]
19
- return @dbclient if @dbclient
20
105
 
21
- # Delegate to parent if present, otherwise fall back to Familia
22
- parent ? parent.dbclient : Familia.dbclient(opts[:logical_database])
106
+ # Build connection chain (not cached due to frozen objects)
107
+ build_connection_chain.handle(uri)
23
108
  end
24
109
 
25
110
  # Produces the full dbkey for this object.
@@ -75,8 +160,69 @@ module Familia
75
160
  # Provides a structured way to "gear down" to run db commands that are
76
161
  # not implemented in our DataType classes since we intentionally don't
77
162
  # have a method_missing method.
163
+ #
164
+ # Enhanced to work seamlessly with transactions and pipelines. When called
165
+ # within a transaction or pipeline context, uses that connection automatically.
166
+ #
167
+ # @yield [Redis, String] Yields the connection and dbkey to the block
168
+ # @return The return value of the block
169
+ #
170
+ # @example Basic usage
171
+ # datatype.direct_access do |conn, key|
172
+ # conn.zadd(key, 100, 'member')
173
+ # end
174
+ #
175
+ # @example Within transaction (automatic context detection)
176
+ # datatype.transaction do |trans_conn|
177
+ # datatype.direct_access do |conn, key|
178
+ # # conn is the same as trans_conn
179
+ # conn.zadd(key, 200, 'member')
180
+ # end
181
+ # end
182
+ #
78
183
  def direct_access
79
- yield(dbclient, dbkey)
184
+ if Fiber[:familia_transaction]
185
+ # Already in transaction, use that connection
186
+ yield(Fiber[:familia_transaction], dbkey)
187
+ elsif Fiber[:familia_pipeline]
188
+ # Already in pipeline, use that connection
189
+ yield(Fiber[:familia_pipeline], dbkey)
190
+ else
191
+ yield(dbclient, dbkey)
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ # Builds the connection chain with handlers in priority order
198
+ #
199
+ # Creates the Chain of Responsibility for connection resolution with
200
+ # DataType-specific handlers. Handlers are checked in order:
201
+ #
202
+ # 1. FiberTransactionHandler - Return active transaction connection
203
+ # 2. FiberConnectionHandler - Use fiber-local connection
204
+ # 3. ProviderConnectionHandler - Delegate to connection provider
205
+ # 4. ParentDelegationHandler - Delegate to parent's connection (primary for owned DataTypes)
206
+ # 5. StandaloneConnectionHandler - Handle standalone DataTypes
207
+ #
208
+ # @return [ResponsibilityChain] Configured connection chain
209
+ #
210
+ def build_connection_chain
211
+ # Create fresh handler instances each time since DataType objects are frozen
212
+ # The chain itself is cached in @connection_chain, so this only runs once
213
+ fiber_connection_handler = Familia::Connection::FiberConnectionHandler.new
214
+ provider_connection_handler = Familia::Connection::ProviderConnectionHandler.new
215
+
216
+ # DataType-specific handlers for parent delegation and standalone usage
217
+ parent_delegation_handler = Familia::Connection::ParentDelegationHandler.new(self)
218
+ standalone_connection_handler = Familia::Connection::StandaloneConnectionHandler.new(self)
219
+
220
+ Familia::Connection::ResponsibilityChain.new
221
+ .add_handler(Familia::Connection::FiberTransactionHandler.instance)
222
+ .add_handler(fiber_connection_handler)
223
+ .add_handler(provider_connection_handler)
224
+ .add_handler(parent_delegation_handler)
225
+ .add_handler(standalone_connection_handler)
80
226
  end
81
227
  end
82
228
  end