familia 2.0.0.pre15 → 2.0.0.pre17

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 (288) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/code-quality.yml +138 -0
  4. data/.github/workflows/code-smells.yml +85 -0
  5. data/.github/workflows/docs.yml +31 -8
  6. data/.gitignore +3 -1
  7. data/.pre-commit-config.yaml +7 -1
  8. data/.reek.yml +98 -0
  9. data/.rubocop.yml +54 -10
  10. data/.talismanrc +9 -0
  11. data/.yardopts +18 -13
  12. data/CHANGELOG.rst +86 -4
  13. data/CLAUDE.md +39 -1
  14. data/Gemfile +6 -5
  15. data/Gemfile.lock +99 -23
  16. data/LICENSE.txt +1 -1
  17. data/README.md +285 -85
  18. data/changelog.d/README.md +2 -2
  19. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  20. data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
  21. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  22. data/docs/archive/README.md +3 -2
  23. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  24. data/docs/conf.py +29 -0
  25. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  26. data/docs/guides/feature-encrypted-fields.md +785 -0
  27. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  28. data/docs/guides/feature-external-identifiers.md +637 -0
  29. data/docs/guides/feature-object-identifiers.md +435 -0
  30. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  31. data/docs/guides/feature-relationships-methods.md +684 -0
  32. data/docs/guides/feature-relationships.md +200 -0
  33. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  34. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  35. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  36. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  37. data/docs/guides/index.md +176 -0
  38. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  39. data/docs/migrating/v2.0.0-pre.md +1 -1
  40. data/docs/migrating/v2.0.0-pre11.md +2 -2
  41. data/docs/migrating/v2.0.0-pre12.md +2 -2
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +624 -20
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +7 -7
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +51 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/class_methods.rb +63 -0
  65. data/lib/familia/data_type/commands.rb +53 -51
  66. data/lib/familia/data_type/connection.rb +83 -0
  67. data/lib/familia/data_type/serialization.rb +108 -107
  68. data/lib/familia/data_type/settings.rb +96 -0
  69. data/lib/familia/data_type/types/counter.rb +1 -1
  70. data/lib/familia/data_type/types/hashkey.rb +15 -11
  71. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  72. data/lib/familia/data_type/types/lock.rb +3 -2
  73. data/lib/familia/data_type/types/sorted_set.rb +128 -14
  74. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
  75. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  76. data/lib/familia/data_type.rb +12 -171
  77. data/lib/familia/distinguisher.rb +85 -0
  78. data/lib/familia/encryption/encrypted_data.rb +15 -24
  79. data/lib/familia/encryption/manager.rb +6 -4
  80. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  81. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  82. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  83. data/lib/familia/encryption/request_cache.rb +7 -7
  84. data/lib/familia/encryption.rb +2 -3
  85. data/lib/familia/errors.rb +9 -3
  86. data/lib/familia/features/autoloader.rb +30 -12
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  89. data/lib/familia/features/encrypted_fields.rb +71 -66
  90. data/lib/familia/features/expiration/extensions.rb +1 -1
  91. data/lib/familia/features/expiration.rb +31 -26
  92. data/lib/familia/features/external_identifier.rb +57 -19
  93. data/lib/familia/features/object_identifier.rb +134 -25
  94. data/lib/familia/features/quantization.rb +16 -21
  95. data/lib/familia/features/relationships/README.md +97 -0
  96. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  98. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
  99. data/lib/familia/features/relationships/indexing.rb +182 -256
  100. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  101. data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
  102. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  103. data/lib/familia/features/relationships/participation.rb +656 -0
  104. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  105. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  106. data/lib/familia/features/relationships.rb +65 -266
  107. data/lib/familia/features/safe_dump.rb +127 -130
  108. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  109. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  110. data/lib/familia/features/transient_fields.rb +10 -7
  111. data/lib/familia/features.rb +10 -14
  112. data/lib/familia/field_type.rb +6 -4
  113. data/lib/familia/horreum/connection.rb +297 -0
  114. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
  115. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
  116. data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
  117. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
  118. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
  119. data/lib/familia/horreum/serialization.rb +172 -0
  120. data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
  121. data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
  122. data/lib/familia/horreum.rb +222 -119
  123. data/lib/familia/json_serializer.rb +0 -1
  124. data/lib/familia/logging.rb +11 -114
  125. data/lib/familia/refinements/dear_json.rb +122 -0
  126. data/lib/familia/refinements/logger_trace.rb +20 -17
  127. data/lib/familia/refinements/stylize_words.rb +65 -0
  128. data/lib/familia/refinements/time_literals.rb +60 -52
  129. data/lib/familia/refinements.rb +2 -1
  130. data/lib/familia/secure_identifier.rb +60 -28
  131. data/lib/familia/settings.rb +83 -7
  132. data/lib/familia/utils.rb +5 -87
  133. data/lib/familia/verifiable_identifier.rb +4 -4
  134. data/lib/familia/version.rb +1 -1
  135. data/lib/familia.rb +72 -14
  136. data/lib/middleware/database_middleware.rb +56 -14
  137. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  138. data/try/configuration/scenarios_try.rb +2 -2
  139. data/try/connection/fiber_context_preservation_try.rb +250 -0
  140. data/try/connection/handler_constraints_try.rb +59 -0
  141. data/try/connection/operation_mode_guards_try.rb +208 -0
  142. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  143. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  144. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  145. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  146. data/try/connection/transaction_mode_strict_try.rb +98 -0
  147. data/try/connection/transaction_mode_warn_try.rb +131 -0
  148. data/try/connection/transaction_modes_try.rb +249 -0
  149. data/try/core/autoloader_try.rb +120 -2
  150. data/try/core/connection_try.rb +10 -10
  151. data/try/core/conventional_inheritance_try.rb +130 -0
  152. data/try/core/create_method_try.rb +15 -23
  153. data/try/core/database_consistency_try.rb +11 -10
  154. data/try/core/errors_try.rb +11 -14
  155. data/try/core/familia_extended_try.rb +2 -2
  156. data/try/core/familia_members_methods_try.rb +76 -0
  157. data/try/core/familia_try.rb +1 -1
  158. data/try/core/isolated_dbclient_try.rb +165 -0
  159. data/try/core/middleware_try.rb +16 -16
  160. data/try/core/persistence_operations_try.rb +4 -4
  161. data/try/core/pools_try.rb +42 -26
  162. data/try/core/secure_identifier_try.rb +28 -24
  163. data/try/core/time_utils_try.rb +10 -10
  164. data/try/core/tools_try.rb +3 -3
  165. data/try/core/utils_try.rb +2 -2
  166. data/try/data_types/boolean_try.rb +4 -4
  167. data/try/data_types/datatype_base_try.rb +0 -2
  168. data/try/data_types/list_try.rb +10 -10
  169. data/try/data_types/sorted_set_try.rb +5 -5
  170. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  171. data/try/data_types/string_try.rb +12 -12
  172. data/try/data_types/unsortedset_try.rb +33 -0
  173. data/try/debugging/cache_behavior_tracer.rb +7 -7
  174. data/try/debugging/debug_aad_process.rb +1 -1
  175. data/try/debugging/debug_concealed_internal.rb +1 -1
  176. data/try/debugging/debug_cross_context.rb +1 -1
  177. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  178. data/try/debugging/encryption_method_tracer.rb +10 -10
  179. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  180. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  181. data/try/encryption/config_persistence_try.rb +2 -2
  182. data/try/encryption/encryption_core_try.rb +19 -19
  183. data/try/encryption/instance_variable_scope_try.rb +1 -1
  184. data/try/encryption/module_loading_try.rb +2 -2
  185. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  186. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  187. data/try/encryption/secure_memory_handling_try.rb +1 -1
  188. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  189. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  190. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  191. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  192. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  193. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  194. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  195. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  196. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  197. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  198. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  199. data/try/features/feature_dependencies_try.rb +3 -3
  200. data/try/features/field_groups_try.rb +244 -0
  201. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  202. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  203. data/try/features/quantization/quantization_try.rb +1 -1
  204. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  205. data/try/features/relationships/indexing_try.rb +443 -0
  206. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  207. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  208. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  209. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  210. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  211. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  212. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  213. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  214. data/try/features/relationships/relationships_performance_try.rb +20 -20
  215. data/try/features/relationships/relationships_try.rb +27 -38
  216. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  217. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  218. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  219. data/try/helpers/test_cleanup.rb +86 -0
  220. data/try/helpers/test_helpers.rb +6 -7
  221. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  222. data/try/horreum/base_try.rb +3 -2
  223. data/try/horreum/commands_try.rb +3 -1
  224. data/try/horreum/defensive_initialization_try.rb +86 -0
  225. data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
  226. data/try/horreum/initialization_try.rb +11 -7
  227. data/try/horreum/relations_try.rb +21 -13
  228. data/try/horreum/serialization_try.rb +12 -11
  229. data/try/horreum/settings_try.rb +2 -0
  230. data/try/integration/cross_component_try.rb +3 -3
  231. data/try/memory/memory_basic_test.rb +1 -1
  232. data/try/memory/memory_docker_ruby_dump.sh +2 -2
  233. data/try/models/customer_safe_dump_try.rb +1 -1
  234. data/try/models/customer_try.rb +13 -15
  235. data/try/models/datatype_base_try.rb +3 -3
  236. data/try/models/familia_object_try.rb +9 -8
  237. data/try/performance/benchmarks_try.rb +2 -2
  238. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  239. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  240. data/try/prototypes/atomic_saves_v4.rb +1 -1
  241. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  242. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  243. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  244. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  245. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  246. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  247. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  248. data/try/prototypes/pooling/pool_siege.rb +11 -11
  249. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  250. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  251. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  252. data/try/refinements/logger_trace_methods_try.rb +44 -0
  253. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  254. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  255. data/try/valkey.conf +26 -0
  256. metadata +92 -52
  257. data/.rubocop_todo.yml +0 -208
  258. data/docs/connection_pooling.md +0 -192
  259. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  260. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  261. data/docs/guides/Feature-System-Autoloading.md +0 -198
  262. data/docs/guides/Home.md +0 -116
  263. data/docs/guides/Relationships-Guide.md +0 -737
  264. data/docs/guides/relationships-methods.md +0 -266
  265. data/docs/reference/auditing_database_commands.rb +0 -228
  266. data/examples/permissions.rb +0 -240
  267. data/lib/familia/features/relationships/cascading.rb +0 -437
  268. data/lib/familia/features/relationships/membership.rb +0 -497
  269. data/lib/familia/features/relationships/permission_management.rb +0 -264
  270. data/lib/familia/features/relationships/querying.rb +0 -615
  271. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  272. data/lib/familia/features/relationships/tracking.rb +0 -418
  273. data/lib/familia/horreum/core/connection.rb +0 -73
  274. data/lib/familia/horreum/core.rb +0 -21
  275. data/lib/familia/refinements/snake_case.rb +0 -40
  276. data/lib/familia/validation/command_recorder.rb +0 -336
  277. data/lib/familia/validation/expectations.rb +0 -519
  278. data/lib/familia/validation/validation_helpers.rb +0 -443
  279. data/lib/familia/validation/validator.rb +0 -412
  280. data/lib/familia/validation.rb +0 -140
  281. data/try/data_types/set_try.rb +0 -33
  282. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  283. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  284. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  285. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  286. data/try/validation/command_validation_try.rb.disabled +0 -207
  287. data/try/validation/performance_validation_try.rb.disabled +0 -324
  288. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -1,7 +1,12 @@
1
1
  # lib/familia/connection.rb
2
2
 
3
- require_relative '../../lib/middleware/database_middleware'
4
- require_relative 'multi_result'
3
+ require_relative 'connection/handlers'
4
+ require_relative 'connection/middleware'
5
+ require_relative 'connection/operations'
6
+ require_relative 'connection/individual_command_proxy'
7
+ require_relative 'connection/operation_core'
8
+ require_relative 'connection/transaction_core'
9
+ require_relative 'connection/pipeline_core'
5
10
 
6
11
  # Familia
7
12
  #
@@ -9,7 +14,8 @@ require_relative 'multi_result'
9
14
  #
10
15
  module Familia
11
16
  @uri = URI.parse 'redis://127.0.0.1:6379'
12
- @database_clients = {}
17
+ @middleware_registered = false
18
+ @middleware_version = 0
13
19
 
14
20
  # The Connection module provides Database connection management for Familia.
15
21
  # It allows easy setup and access to Database clients across different URIs
@@ -18,222 +24,86 @@ module Familia
18
24
  # @return [URI] The default URI for Database connections
19
25
  attr_reader :uri
20
26
 
21
- # @return [Hash] A hash of Database clients, keyed by server ID
22
- attr_reader :database_clients
23
-
24
- # @return [Boolean] Whether Database command logging is enabled
25
- attr_accessor :enable_database_logging
26
-
27
- # @return [Boolean] Whether Database command counter is enabled
28
- attr_accessor :enable_database_counter
29
-
30
27
  # @return [Proc] A callable that provides Database connections
31
- attr_accessor :connection_provider
28
+ # The provider should accept a URI string and return a Redis connection
29
+ # already connected to the correct database specified in the URI.
30
+ #
31
+ # @example Setting a connection provider
32
+ # Familia.connection_provider = ->(uri) do
33
+ # pool = ConnectionPool.new { Redis.new(url: uri) }
34
+ # pool.with { |conn| conn }
35
+ # end
36
+ attr_reader :connection_provider
32
37
 
33
- # @return [Boolean] Whether to require external connections (no fallback)
34
- attr_accessor :connection_required
38
+ # Sets the connection provider and bumps middleware version
39
+ def connection_provider=(provider)
40
+ @connection_provider = provider
41
+ increment_middleware_version! if provider
42
+ @connection_chain = nil # Force rebuild of chain
43
+ end
35
44
 
36
45
  # Sets the default URI for Database connections.
37
46
  #
38
47
  # NOTE: uri is not a property of the Settings module b/c it's not
39
48
  # configured in class defintions like default_expiration or logical DB index.
40
49
  #
41
- # @param v [String, URI] The new default URI
42
- # @example
43
- # Familia.uri = 'redis://localhost:6379'
50
+ # @param uri [String, URI] The new default URI
51
+ # @example Familia.uri = 'redis://localhost:6379'
44
52
  def uri=(uri)
45
53
  @uri = normalize_uri(uri)
46
54
  end
47
55
  alias url uri
48
56
  alias url= uri=
49
57
 
50
- # Establishes a connection to a Database server.
58
+ # Creates a new Database connection instance.
59
+ #
60
+ # This method always creates a fresh connection and does not use caching.
61
+ # Each call returns a new Redis client instance that you are responsible
62
+ # for managing and closing when done.
51
63
  #
52
64
  # @param uri [String, URI, nil] The URI of the Database server to connect to.
53
- # If nil, uses the default URI from `@database_clients` or `Familia.uri`.
54
- # @return [Redis] The connected Database client.
65
+ # If nil, uses the default URI from Familia.uri.
66
+ # @return [Redis] A new Database client connection.
55
67
  # @raise [ArgumentError] If no URI is specified.
56
- # @example
57
- # Familia.connect('redis://localhost:6379')
58
- def connect(uri = nil)
68
+ #
69
+ # @example Creating a new connection
70
+ # client = Familia.create_dbclient('redis://localhost:6379')
71
+ # client.ping
72
+ # client.close
73
+ #
74
+ def create_dbclient(uri = nil)
59
75
  parsed_uri = normalize_uri(uri)
60
76
 
61
- if Familia.enable_database_logging
62
- DatabaseLogger.logger = Familia.logger
63
- RedisClient.register(DatabaseLogger)
64
- end
65
-
66
- if Familia.enable_database_counter
67
- # NOTE: This middleware uses AtommicFixnum from concurrent-ruby which is
68
- # less contentious than Mutex-based counters. Safe for
69
- RedisClient.register(DatabaseCommandCounter)
70
- end
77
+ # Register middleware only once, globally
78
+ register_middleware_once
71
79
 
72
80
  Redis.new(parsed_uri.conf)
73
81
  end
82
+ alias connect create_dbclient # backwards compatibility
83
+ alias isolated_dbclient create_dbclient # matches with_isolated_dbclient api
74
84
 
75
- def reconnect(uri = nil)
76
- parsed_uri = normalize_uri(uri)
77
- serverid = parsed_uri.serverid
78
-
79
- # Close the existing connection if it exists
80
- @database_clients[serverid].close if @database_clients.key?(serverid)
81
- @database_clients.delete(serverid)
82
-
83
- connect(parsed_uri)
84
- end
85
-
86
- # Retrieves a Database connection from the appropriate pool.
85
+ # Retrieves a Database connection using the Chain of Responsibility pattern.
87
86
  # Handles DB selection automatically based on the URI.
88
87
  #
89
88
  # @return [Redis] The Database client for the specified URI
90
- # @example
91
- # Familia.dbclient('redis://localhost:6379/1')
89
+ # @example Familia.dbclient('redis://localhost:6379/1')
92
90
  # Familia.dbclient(2) # Use DB 2 with default server
93
91
  def dbclient(uri = nil)
94
- # First priority: Thread-local connection (middleware pattern)
95
- return Thread.current[:familia_connection] if Thread.current.key?(:familia_connection)
96
-
97
- # Second priority: Connection provider
98
- if connection_provider
99
- # Always pass normalized URI with database to provider
100
- # Provider MUST return connection already on the correct database
101
- parsed_uri = normalize_uri(uri)
102
- client = connection_provider.call(parsed_uri.to_s)
103
-
104
- # In debug mode, verify the provider honored the contract
105
- if Familia.debug? && client.respond_to?(:client)
106
- current_db = client.connection[:db]
107
- expected_db = parsed_uri.db || 0
108
- Familia.ld "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
109
- if current_db != expected_db
110
- Familia.warn "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
111
- end
112
- end
113
-
114
- return client
115
- end
116
-
117
- # Third priority: Fallback behavior or error
118
- raise Familia::NoConnectionAvailable, 'No connection available.' if connection_required
119
-
120
- # Legacy behavior: create connection
121
- parsed_uri = normalize_uri(uri)
122
- serverid = parsed_uri.serverid
123
-
124
- @database_clients[serverid] ||= connect(parsed_uri)
125
- end
126
-
127
- # Executes Database commands atomically within a transaction (MULTI/EXEC).
128
- #
129
- # Database transactions queue commands and execute them atomically as a single unit.
130
- # All commands succeed together or all fail together, ensuring data consistency.
131
- #
132
- # @yield [Redis] The Database transaction connection
133
- # @return [Array] Results of all commands executed in the transaction
134
- #
135
- # @example Basic transaction usage
136
- # Familia.transaction do |trans|
137
- # trans.set("key1", "value1")
138
- # trans.incr("counter")
139
- # trans.lpush("list", "item")
140
- # end
141
- # # Returns: ["OK", 2, 1] - results of all commands
142
- #
143
- # @note **Comparison of Database batch operations:**
144
- #
145
- # | Feature | Multi/Exec | Pipeline |
146
- # |-----------------|-----------------|-----------------|
147
- # | Atomicity | Yes | No |
148
- # | Performance | Good | Better |
149
- # | Error handling | All-or-nothing | Per-command |
150
- # | Use case | Data consistency| Bulk operations |
151
- #
152
- def transaction(&)
153
- block_result = nil
154
- result = dbclient.multi do |conn|
155
- Fiber[:familia_transaction] = conn
156
- begin
157
- block_result = yield(conn)
158
- ensure
159
- Fiber[:familia_transaction] = nil # cleanup reference
160
- end
161
- end
162
- # Return the multi result which contains the transaction results
163
- result
92
+ @connection_chain ||= build_connection_chain
93
+ @connection_chain.handle(uri)
164
94
  end
165
- alias multi transaction
166
95
 
167
- # Executes Database commands in a pipeline for improved performance.
168
- #
169
- # Pipelines send multiple commands without waiting for individual responses,
170
- # reducing network round-trips. Commands execute independently and can
171
- # succeed or fail without affecting other commands in the pipeline.
172
- #
173
- # @yield [Redis] The Database pipeline connection
174
- # @return [Array] Results of all commands executed in the pipeline
175
- #
176
- # @example Basic pipeline usage
177
- # Familia.pipeline do |pipe|
178
- # pipe.set("key1", "value1")
179
- # pipe.incr("counter")
180
- # pipe.lpush("list", "item")
181
- # end
182
- # # Returns: ["OK", 2, 1] - results of all commands
183
- #
184
- # @example Error handling - commands succeed/fail independently
185
- # results = Familia.pipeline do |conn|
186
- # conn.set("valid_key", "value") # This will succeed
187
- # conn.incr("string_key") # This will fail (wrong type)
188
- # conn.set("another_key", "value2") # This will still succeed
189
- # end
190
- # # Returns: ["OK", Redis::CommandError, "OK"]
191
- # # Notice how the error doesn't prevent other commands from executing
192
- #
193
- # @example Contrast with transaction behavior
194
- # results = Familia.transaction do |conn|
195
- # conn.set("inventory:item1", 100)
196
- # conn.incr("invalid_key") # Fails, rolls back everything
197
- # conn.set("inventory:item2", 200) # Won't be applied
198
- # end
199
- # # Result: neither item1 nor item2 are set due to the error
200
- #
201
- def pipeline(&)
202
- block_result = nil
203
- result = dbclient.pipelined do |conn|
204
- Fiber[:familia_pipeline] = conn
205
- begin
206
- block_result = yield(conn)
207
- ensure
208
- Fiber[:familia_pipeline] = nil # cleanup reference
209
- end
210
- end
211
- # Return the pipeline result which contains the command results
212
- result
96
+ # Builds the connection chain with handlers in priority order
97
+ def build_connection_chain
98
+ ResponsibilityChain.new
99
+ .add_handler(Familia::Connection::FiberTransactionHandler.new)
100
+ .add_handler(FiberConnectionHandler.new)
101
+ .add_handler(ProviderConnectionHandler.new)
102
+ .add_handler(CreateConnectionHandler.new)
213
103
  end
214
104
 
215
- # Provides explicit access to a Database connection.
216
- #
217
- # This method is useful when you need direct access to a connection
218
- # for operations not covered by other methods. The connection is
219
- # properly managed and returned to the pool (if using connection_provider).
220
- #
221
- # @yield [Redis] A Database connection
222
- # @return The result of the block
223
- #
224
- # @example Using with_connection for custom operations
225
- # Familia.with_connection do |conn|
226
- # conn.set("custom_key", "value")
227
- # conn.expire("custom_key", 3600)
228
- # end
229
- #
230
- def with_connection(&)
231
- yield dbclient
232
- end
233
-
234
- private
235
-
236
105
  # Normalizes various URI formats to a consistent URI object
106
+ # Made public so handlers can use it
237
107
  def normalize_uri(uri)
238
108
  case uri
239
109
  when Integer
@@ -250,5 +120,9 @@ module Familia
250
120
  raise ArgumentError, "Invalid URI type: #{uri.class.name}"
251
121
  end
252
122
  end
123
+
124
+ # Extend self with submodules to make their methods available as module methods
125
+ include Familia::Connection::Middleware
126
+ include Familia::Connection::Operations
253
127
  end
254
128
  end
@@ -0,0 +1,63 @@
1
+ # lib/familia/data_type/definition.rb
2
+
3
+ module Familia
4
+ class DataType
5
+ # ClassMethods - Class-level DSL methods for defining DataType behavior
6
+ #
7
+ # This module is extended into classes that inherit from Familia::DataType,
8
+ # providing class methods for type registration, configuration, and inheritance.
9
+ #
10
+ # Key features:
11
+ # * Type registration system for creating DataType subclasses
12
+ # * Database and connection configuration
13
+ # * Inheritance hooks for propagating settings
14
+ # * Option validation and filtering
15
+ #
16
+ module ClassMethods
17
+ attr_accessor :parent, :suffix, :prefix, :uri
18
+ attr_writer :logical_database
19
+
20
+ # To be called inside every class that inherits DataType
21
+ # +methname+ is the term used for the class and instance methods
22
+ # that are created for the given +klass+ (e.g. set, list, etc)
23
+ def register(klass, methname)
24
+ Familia.trace :REGISTER, nil, "[#{self}] Registering #{klass} as #{methname.inspect}" if Familia.debug?
25
+
26
+ @registered_types[methname] = klass
27
+ end
28
+
29
+ # Get the registered type class from a given method name
30
+ # +methname+ is the method name used to register the class (e.g. :set, :list, etc)
31
+ # Returns the registered class or nil if not found
32
+ def registered_type(methname)
33
+ @registered_types[methname]
34
+ end
35
+
36
+ def logical_database(val = nil)
37
+ @logical_database = val unless val.nil?
38
+ @logical_database || parent&.logical_database
39
+ end
40
+
41
+ def uri(val = nil)
42
+ @uri = val unless val.nil?
43
+ @uri || (parent ? parent.uri : Familia.uri)
44
+ end
45
+
46
+ def inherited(obj)
47
+ Familia.trace :DATATYPE, nil, "#{obj} is my kinda type" if Familia.debug?
48
+ obj.logical_database = logical_database
49
+ obj.default_expiration = default_expiration # method added via Features::Expiration
50
+ obj.uri = uri
51
+ super
52
+ end
53
+
54
+ def valid_keys_only(opts)
55
+ opts.slice(*DataType.valid_options)
56
+ end
57
+
58
+ def relations?
59
+ @has_related_fields ||= false
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,56 +1,58 @@
1
1
  # lib/familia/data_type/commands.rb
2
2
 
3
- class Familia::DataType
4
- # Must be included in all DataType classes to provide Redis
5
- # commands. The class must have a dbkey method.
6
- module Commands
7
- def move(logical_database)
8
- dbclient.move dbkey, logical_database
9
- end
10
-
11
- def rename(newkey)
12
- dbclient.rename dbkey, newkey
13
- end
14
-
15
- def renamenx(newkey)
16
- dbclient.renamenx dbkey, newkey
17
- end
18
-
19
- def type
20
- dbclient.type dbkey
21
- end
22
-
23
- # Deletes the entire dbkey
24
- # @return [Boolean] true if the key was deleted, false otherwise
25
- def delete!
26
- Familia.trace :DELETE!, dbclient, uri, caller(1..1) if Familia.debug?
27
- ret = dbclient.del dbkey
28
- ret.positive?
29
- end
30
- alias clear delete!
31
-
32
- def exists?
33
- dbclient.exists(dbkey) && !size.zero?
34
- end
35
-
36
- def current_expiration
37
- dbclient.ttl dbkey
38
- end
39
-
40
- def expire(sec)
41
- dbclient.expire dbkey, sec.to_i
42
- end
43
-
44
- def expireat(unixtime)
45
- dbclient.expireat dbkey, unixtime
46
- end
47
-
48
- def persist
49
- dbclient.persist dbkey
50
- end
51
-
52
- def echo(meth, trace)
53
- dbclient.echo "[#{self.class}##{meth}] #{trace} (#{@opts[:class]}#)"
3
+ module Familia
4
+ class DataType
5
+ # Must be included in all DataType classes to provide Valkey/Redis
6
+ # commands. The class must have a dbkey method.
7
+ module Commands
8
+ def move(logical_database)
9
+ dbclient.move dbkey, logical_database
10
+ end
11
+
12
+ def rename(newkey)
13
+ dbclient.rename dbkey, newkey
14
+ end
15
+
16
+ def renamenx(newkey)
17
+ dbclient.renamenx dbkey, newkey
18
+ end
19
+
20
+ def type
21
+ dbclient.type dbkey
22
+ end
23
+
24
+ # Deletes the entire dbkey
25
+ # @return [Boolean] true if the key was deleted, false otherwise
26
+ def delete!
27
+ Familia.trace :DELETE!, nil, uri if Familia.debug?
28
+ ret = dbclient.del dbkey
29
+ ret.positive?
30
+ end
31
+ alias clear delete!
32
+
33
+ def exists?
34
+ dbclient.exists(dbkey) && !size.zero?
35
+ end
36
+
37
+ def current_expiration
38
+ dbclient.ttl dbkey
39
+ end
40
+
41
+ def expire(sec)
42
+ dbclient.expire dbkey, sec.to_i
43
+ end
44
+
45
+ def expireat(unixtime)
46
+ dbclient.expireat dbkey, unixtime
47
+ end
48
+
49
+ def persist
50
+ dbclient.persist dbkey
51
+ end
52
+
53
+ def echo(*args)
54
+ dbclient.echo "[#{self.class}] #{args.join(' ')} (#{opts&.fetch(:class, '<no opts>')})"
55
+ end
54
56
  end
55
57
  end
56
58
  end
@@ -0,0 +1,83 @@
1
+ # lib/familia/data_type/connection.rb
2
+
3
+ module Familia
4
+ class DataType
5
+ # Connection - Instance-level connection and key generation methods
6
+ #
7
+ # This module provides instance methods for database connection resolution
8
+ # and Redis key generation for DataType objects.
9
+ #
10
+ # Key features:
11
+ # * Database connection resolution with Chain of Responsibility pattern
12
+ # * Redis key generation based on parent context
13
+ # * Direct database access for advanced operations
14
+ #
15
+ module Connection
16
+ # TODO: Replace with Chain of Responsibility pattern
17
+ def dbclient
18
+ return Fiber[:familia_transaction] if Fiber[:familia_transaction]
19
+ return @dbclient if @dbclient
20
+
21
+ # Delegate to parent if present, otherwise fall back to Familia
22
+ parent ? parent.dbclient : Familia.dbclient(opts[:logical_database])
23
+ end
24
+
25
+ # Produces the full dbkey for this object.
26
+ #
27
+ # @return [String] The full dbkey.
28
+ #
29
+ # This method determines the appropriate dbkey based on the context of the DataType object:
30
+ #
31
+ # 1. If a hardcoded key is set in the options, it returns that key.
32
+ # 2. For instance-level DataType objects, it uses the parent instance's dbkey method.
33
+ # 3. For class-level DataType objects, it uses the parent class's dbkey method.
34
+ # 4. For standalone DataType objects, it uses the keystring as the full dbkey.
35
+ #
36
+ # For class-level DataType objects (parent_class? == true):
37
+ # - The suffix is optional and used to differentiate between different types of objects.
38
+ # - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
39
+ # - If a nil suffix is explicitly passed, it won't appear in the resulting dbkey.
40
+ # - Passing nil as the suffix is how class-level DataType objects are created without
41
+ # the global default 'object' suffix.
42
+ #
43
+ # @example Instance-level DataType
44
+ # user_instance.some_datatype.dbkey # => "user:123:some_datatype"
45
+ #
46
+ # @example Class-level DataType
47
+ # User.some_datatype.dbkey # => "user:some_datatype"
48
+ #
49
+ # @example Standalone DataType
50
+ # DataType.new("mykey").dbkey # => "mykey"
51
+ #
52
+ # @example Class-level DataType with explicit nil suffix
53
+ # User.dbkey("123", nil) # => "user:123"
54
+ #
55
+ def dbkey
56
+ # Return the hardcoded key if it's set. This is useful for
57
+ # support legacy keys that aren't derived in the same way.
58
+ return opts[:dbkey] if opts[:dbkey]
59
+
60
+ if parent_instance?
61
+ # This is an instance-level datatype object so the parent instance's
62
+ # dbkey method is defined in Familia::Horreum::InstanceMethods.
63
+ parent.dbkey(keystring)
64
+ elsif parent_class?
65
+ # This is a class-level datatype object so the parent class' dbkey
66
+ # method is defined in Familia::Horreum::DefinitionMethods.
67
+ parent.dbkey(keystring, nil)
68
+ else
69
+ # This is a standalone DataType object where it's keystring
70
+ # is the full database key (dbkey).
71
+ keystring
72
+ end
73
+ end
74
+
75
+ # Provides a structured way to "gear down" to run db commands that are
76
+ # not implemented in our DataType classes since we intentionally don't
77
+ # have a method_missing method.
78
+ def direct_access
79
+ yield(dbclient, dbkey)
80
+ end
81
+ end
82
+ end
83
+ end