familia 2.0.0.pre14 → 2.0.0.pre16

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 (276) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +66 -6
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +4 -4
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre13.md +1 -1
  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 +623 -19
  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 +6 -6
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +49 -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/commands.rb +53 -51
  65. data/lib/familia/data_type/serialization.rb +108 -107
  66. data/lib/familia/data_type/types/counter.rb +1 -1
  67. data/lib/familia/data_type/types/hashkey.rb +13 -10
  68. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  69. data/lib/familia/data_type/types/lock.rb +3 -2
  70. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  71. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  72. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  73. data/lib/familia/data_type.rb +75 -47
  74. data/lib/familia/distinguisher.rb +85 -0
  75. data/lib/familia/encryption/encrypted_data.rb +15 -24
  76. data/lib/familia/encryption/manager.rb +6 -4
  77. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  78. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  79. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  80. data/lib/familia/encryption/request_cache.rb +7 -7
  81. data/lib/familia/encryption.rb +2 -3
  82. data/lib/familia/errors.rb +9 -3
  83. data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
  84. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  85. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  86. data/lib/familia/features/encrypted_fields.rb +68 -66
  87. data/lib/familia/features/expiration/extensions.rb +61 -0
  88. data/lib/familia/features/expiration.rb +35 -87
  89. data/lib/familia/features/external_identifier.rb +11 -12
  90. data/lib/familia/features/object_identifier.rb +58 -20
  91. data/lib/familia/features/quantization.rb +17 -22
  92. data/lib/familia/features/relationships/README.md +97 -0
  93. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  94. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  95. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  96. data/lib/familia/features/relationships/indexing.rb +176 -256
  97. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  98. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  99. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  100. data/lib/familia/features/relationships/participation.rb +656 -0
  101. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  102. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  103. data/lib/familia/features/relationships.rb +69 -271
  104. data/lib/familia/features/safe_dump.rb +127 -132
  105. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  106. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  107. data/lib/familia/features/transient_fields.rb +5 -5
  108. data/lib/familia/features.rb +21 -21
  109. data/lib/familia/field_type.rb +24 -4
  110. data/lib/familia/horreum/core/connection.rb +229 -26
  111. data/lib/familia/horreum/core/database_commands.rb +27 -17
  112. data/lib/familia/horreum/core/serialization.rb +40 -20
  113. data/lib/familia/horreum/core/utils.rb +2 -1
  114. data/lib/familia/horreum/shared/settings.rb +2 -1
  115. data/lib/familia/horreum/subclass/definition.rb +33 -45
  116. data/lib/familia/horreum/subclass/management.rb +72 -24
  117. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  118. data/lib/familia/horreum.rb +196 -114
  119. data/lib/familia/json_serializer.rb +0 -1
  120. data/lib/familia/logging.rb +11 -114
  121. data/lib/familia/refinements/dear_json.rb +122 -0
  122. data/lib/familia/refinements/logger_trace.rb +20 -17
  123. data/lib/familia/refinements/stylize_words.rb +65 -0
  124. data/lib/familia/refinements/time_literals.rb +60 -52
  125. data/lib/familia/refinements.rb +2 -1
  126. data/lib/familia/secure_identifier.rb +60 -28
  127. data/lib/familia/settings.rb +83 -7
  128. data/lib/familia/utils.rb +5 -87
  129. data/lib/familia/verifiable_identifier.rb +4 -4
  130. data/lib/familia/version.rb +1 -1
  131. data/lib/familia.rb +72 -15
  132. data/lib/middleware/database_middleware.rb +56 -14
  133. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  134. data/try/configuration/scenarios_try.rb +1 -1
  135. data/try/connection/fiber_context_preservation_try.rb +250 -0
  136. data/try/connection/handler_constraints_try.rb +59 -0
  137. data/try/connection/operation_mode_guards_try.rb +208 -0
  138. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  139. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  140. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  141. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  142. data/try/connection/transaction_mode_strict_try.rb +98 -0
  143. data/try/connection/transaction_mode_warn_try.rb +131 -0
  144. data/try/connection/transaction_modes_try.rb +249 -0
  145. data/try/core/autoloader_try.rb +129 -11
  146. data/try/core/connection_try.rb +7 -7
  147. data/try/core/conventional_inheritance_try.rb +130 -0
  148. data/try/core/create_method_try.rb +15 -23
  149. data/try/core/database_consistency_try.rb +10 -10
  150. data/try/core/errors_try.rb +8 -11
  151. data/try/core/familia_extended_try.rb +2 -2
  152. data/try/core/familia_members_methods_try.rb +76 -0
  153. data/try/core/isolated_dbclient_try.rb +165 -0
  154. data/try/core/middleware_try.rb +16 -16
  155. data/try/core/persistence_operations_try.rb +4 -4
  156. data/try/core/pools_try.rb +42 -26
  157. data/try/core/secure_identifier_try.rb +28 -24
  158. data/try/core/time_utils_try.rb +10 -10
  159. data/try/core/tools_try.rb +1 -1
  160. data/try/core/utils_try.rb +2 -2
  161. data/try/data_types/boolean_try.rb +4 -4
  162. data/try/data_types/datatype_base_try.rb +0 -2
  163. data/try/data_types/list_try.rb +10 -10
  164. data/try/data_types/sorted_set_try.rb +5 -5
  165. data/try/data_types/string_try.rb +12 -12
  166. data/try/data_types/unsortedset_try.rb +33 -0
  167. data/try/debugging/cache_behavior_tracer.rb +7 -7
  168. data/try/debugging/debug_aad_process.rb +1 -1
  169. data/try/debugging/debug_concealed_internal.rb +1 -1
  170. data/try/debugging/debug_cross_context.rb +1 -1
  171. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  172. data/try/debugging/encryption_method_tracer.rb +10 -10
  173. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  174. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  175. data/try/encryption/config_persistence_try.rb +2 -2
  176. data/try/encryption/encryption_core_try.rb +19 -19
  177. data/try/encryption/instance_variable_scope_try.rb +1 -1
  178. data/try/encryption/module_loading_try.rb +2 -2
  179. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  180. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  181. data/try/encryption/secure_memory_handling_try.rb +1 -1
  182. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  183. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  184. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  185. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  186. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  187. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  188. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  189. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  190. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  191. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  192. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  193. data/try/features/feature_dependencies_try.rb +3 -3
  194. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  195. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  196. data/try/features/quantization/quantization_try.rb +1 -1
  197. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  198. data/try/features/relationships/indexing_try.rb +433 -0
  199. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  200. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  201. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  202. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  203. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  204. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  205. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  206. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  207. data/try/features/relationships/relationships_performance_try.rb +20 -20
  208. data/try/features/relationships/relationships_try.rb +27 -38
  209. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  210. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  211. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  212. data/try/helpers/test_cleanup.rb +86 -0
  213. data/try/helpers/test_helpers.rb +3 -3
  214. data/try/horreum/base_try.rb +3 -2
  215. data/try/horreum/commands_try.rb +1 -1
  216. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  217. data/try/horreum/initialization_try.rb +11 -7
  218. data/try/horreum/relations_try.rb +21 -13
  219. data/try/horreum/serialization_try.rb +12 -11
  220. data/try/integration/cross_component_try.rb +3 -3
  221. data/try/memory/memory_basic_test.rb +1 -1
  222. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  223. data/try/models/customer_safe_dump_try.rb +1 -1
  224. data/try/models/customer_try.rb +8 -10
  225. data/try/models/datatype_base_try.rb +3 -3
  226. data/try/models/familia_object_try.rb +9 -8
  227. data/try/performance/benchmarks_try.rb +2 -2
  228. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  229. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  230. data/try/prototypes/atomic_saves_v4.rb +1 -1
  231. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  232. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  234. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  235. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  236. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  237. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  238. data/try/prototypes/pooling/pool_siege.rb +11 -11
  239. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  240. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  241. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  242. data/try/refinements/logger_trace_methods_try.rb +44 -0
  243. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  244. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  245. metadata +77 -45
  246. data/.rubocop_todo.yml +0 -208
  247. data/docs/connection_pooling.md +0 -192
  248. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  249. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  250. data/docs/guides/Feature-System-Autoloading.md +0 -228
  251. data/docs/guides/Home.md +0 -116
  252. data/docs/guides/Relationships-Guide.md +0 -737
  253. data/docs/guides/relationships-methods.md +0 -266
  254. data/docs/reference/auditing_database_commands.rb +0 -228
  255. data/examples/permissions.rb +0 -240
  256. data/lib/familia/features/autoloadable.rb +0 -113
  257. data/lib/familia/features/relationships/cascading.rb +0 -437
  258. data/lib/familia/features/relationships/membership.rb +0 -497
  259. data/lib/familia/features/relationships/permission_management.rb +0 -264
  260. data/lib/familia/features/relationships/querying.rb +0 -615
  261. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  262. data/lib/familia/features/relationships/tracking.rb +0 -418
  263. data/lib/familia/refinements/snake_case.rb +0 -40
  264. data/lib/familia/validation/command_recorder.rb +0 -336
  265. data/lib/familia/validation/expectations.rb +0 -519
  266. data/lib/familia/validation/validation_helpers.rb +0 -443
  267. data/lib/familia/validation/validator.rb +0 -412
  268. data/lib/familia/validation.rb +0 -140
  269. data/try/data_types/set_try.rb +0 -33
  270. data/try/features/autoloadable/autoloadable_try.rb +0 -61
  271. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  272. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
  273. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  274. data/try/validation/command_validation_try.rb.disabled +0 -207
  275. data/try/validation/performance_validation_try.rb.disabled +0 -324
  276. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -2,25 +2,61 @@
2
2
 
3
3
  module Familia
4
4
  class Horreum
5
- # Connection: Valkey connection management for Horreum instances
6
- # Provides both instance and class-level connection methods
5
+ # Connection - Mixed instance and class-level methods for Valkey connection management
6
+ # Provides connection handling, transactions, and URI normalization for both
7
+ # class-level operations (e.g., Customer.dbclient) and instance-level operations
8
+ # (e.g., customer.dbclient)
7
9
  module Connection
8
10
  attr_reader :uri
9
11
 
10
- # Returns the Database connection for the class.
12
+ # Normalizes various URI formats to a consistent URI object
13
+ # Considers the class/instance logical_database when uri is nil or Integer
14
+ def normalize_uri(uri)
15
+ case uri
16
+ when Integer
17
+ new_uri = Familia.uri.dup
18
+ new_uri.db = uri
19
+ new_uri
20
+ when ->(obj) { obj.is_a?(String) || obj.instance_of?(::String) }
21
+ URI.parse(uri)
22
+ when URI
23
+ uri
24
+ when nil
25
+ # Use logical_database if available, otherwise fall back to Familia.uri
26
+ if respond_to?(:logical_database) && logical_database
27
+ new_uri = Familia.uri.dup
28
+ new_uri.db = logical_database
29
+ new_uri
30
+ else
31
+ Familia.uri
32
+ end
33
+ else
34
+ raise ArgumentError, "Invalid URI type: #{uri.class.name}"
35
+ end
36
+ end
37
+
38
+ # Creates a new Database connection instance using the class/instance configuration
39
+ def create_dbclient(uri = nil)
40
+ parsed_uri = normalize_uri(uri)
41
+ Familia.create_dbclient(parsed_uri)
42
+ end
43
+
44
+ # Returns the Database connection for the class using Chain of Responsibility pattern.
11
45
  #
12
- # This method retrieves the Database connection instance for the class. If no
13
- # connection is set, it initializes a new connection using the provided URI
14
- # or database configuration.
46
+ # This method uses a chain of handlers to resolve connections in priority order:
47
+ # 1. FiberTransactionHandler - Fiber[:familia_transaction] (active transaction)
48
+ # 2. DefaultConnectionHandler - Horreum model class-level @dbclient
49
+ # 3. GlobalFallbackHandler - Familia.dbclient(uri || logical_database) (global fallback)
15
50
  #
16
51
  # @return [Redis] the Database connection instance.
17
52
  #
18
- def dbclient
19
- Fiber[:familia_transaction] || @dbclient || Familia.dbclient(uri || logical_database)
53
+ def dbclient(uri = nil)
54
+ @class_connection_chain ||= build_connection_chain
55
+ @class_connection_chain.handle(uri)
20
56
  end
21
57
 
22
58
  def connect(*)
23
- Familia.connect(*)
59
+ create_dbclient(*)
24
60
  end
25
61
 
26
62
  def uri=(uri)
@@ -46,27 +82,194 @@ module Familia
46
82
  #
47
83
  # @note This method works with the global Familia.transaction context when available
48
84
  #
85
+ # Executes a Redis transaction (MULTI/EXEC) using this object's connection context.
86
+ #
87
+ # Provides atomic execution of multiple Redis commands with automatic connection
88
+ # management and operation mode enforcement. Uses the object's database and
89
+ # connection settings. Returns a MultiResult object for consistency with global methods.
90
+ #
91
+ # @param [Proc] block The block containing Redis commands to execute atomically
92
+ # @yield [Redis] conn The Redis connection configured for transaction mode
93
+ # @return [MultiResult] Result object with success status and command results
94
+ #
95
+ # @raise [Familia::OperationModeError] When called with incompatible connection handlers
96
+ # (e.g., FiberConnectionHandler or DefaultConnectionHandler that don't support transactions)
97
+ #
98
+ # @example Basic instance transaction
99
+ # customer = Customer.new(custid: 'cust_123')
100
+ # result = customer.transaction do |conn|
101
+ # conn.hset(customer.dbkey, 'name', 'John Doe')
102
+ # conn.hset(customer.dbkey, 'email', 'john@example.com')
103
+ # conn.hget(customer.dbkey, 'name')
104
+ # end
105
+ # result.successful? # => true
106
+ # result.results # => ["OK", "OK", "John Doe"]
107
+ #
108
+ # @example Using with object's database context
109
+ # class Customer < Familia::Horreum
110
+ # logical_database 5 # Use database 5
111
+ # field :name
112
+ # field :email
113
+ # end
114
+ #
115
+ # customer = Customer.new(custid: 'cust_456')
116
+ # result = customer.transaction do |conn|
117
+ # # Commands automatically execute in database 5
118
+ # conn.hset(customer.dbkey, 'status', 'active')
119
+ # conn.sadd('active_customers', customer.identifier)
120
+ # end
121
+ # result.successful? # => true
122
+ #
123
+ # @example Reentrant behavior with global transactions
124
+ # customer = Customer.new(custid: 'cust_789')
125
+ #
126
+ # # When called within a global transaction, reuses the transaction connection
127
+ # result = Familia.transaction do |global_conn|
128
+ # global_conn.set('global_key', 'value')
129
+ #
130
+ # # This reuses the same transaction connection
131
+ # customer.transaction do |local_conn|
132
+ # local_conn.hset(customer.dbkey, 'updated', Time.now.to_i)
133
+ # 'local_return_value' # Returned directly in nested context
134
+ # end
135
+ # end
136
+ #
137
+ # @note Connection Inheritance:
138
+ # - Uses object's logical_database setting if configured
139
+ # - Inherits class-level database settings
140
+ # - Falls back to instance-level dbclient if set
141
+ # - Uses global connection chain as final fallback
142
+ #
143
+ # @note Transaction Context:
144
+ # - When called outside global transaction: Creates local MultiResult
145
+ # - When called inside global transaction: Yields to existing transaction
146
+ # - Maintains proper Fiber-local state for nested calls
147
+ #
148
+ # @see Familia.transaction For global transaction method
149
+ # @see MultiResult For details on the return value structure
150
+ # @see #batch_update For similar atomic field updates with MultiResult
49
151
  def transaction(&)
50
- # If we're already in a Familia.transaction context, just yield the multi connection
51
- if Fiber[:familia_transaction]
52
- yield(Fiber[:familia_transaction])
53
- else
54
- # Otherwise, create a local transaction
55
- block_result = dbclient.multi(&)
56
- end
57
- block_result
152
+ Familia::Connection::TransactionCore.execute_transaction(-> { dbclient }, &)
58
153
  end
59
154
  alias multi transaction
60
155
 
61
- def pipeline(&)
62
- # If we're already in a Familia.pipeline context, just yield the pipeline connection
63
- if Fiber[:familia_pipeline]
64
- yield(Fiber[:familia_pipeline])
65
- else
66
- # Otherwise, create a local transaction
67
- block_result = dbclient.pipeline(&)
68
- end
69
- block_result
156
+ # Executes Redis commands in a pipeline using this object's connection context.
157
+ #
158
+ # Batches multiple Redis commands together and sends them in a single network
159
+ # round-trip for improved performance. Uses the object's database and connection
160
+ # settings. Returns a MultiResult object for consistency with global methods.
161
+ #
162
+ # @param [Proc] block The block containing Redis commands to execute in pipeline
163
+ # @yield [Redis] conn The Redis connection configured for pipelined mode
164
+ # @return [MultiResult] Result object with success status and command results
165
+ #
166
+ # @raise [Familia::OperationModeError] When called with incompatible connection handlers
167
+ # (e.g., FiberConnectionHandler or CachedConnectionHandler that don't support pipelines)
168
+ #
169
+ # @example Basic instance pipeline
170
+ # customer = Customer.new(custid: 'cust_123')
171
+ # result = customer.pipelined do |conn|
172
+ # conn.hset(customer.dbkey, 'last_login', Time.now.to_i)
173
+ # conn.hincrby(customer.dbkey, 'login_count', 1)
174
+ # conn.sadd('recent_logins', customer.identifier)
175
+ # conn.hget(customer.dbkey, 'login_count')
176
+ # end
177
+ # result.successful? # => true
178
+ # result.results # => ["OK", 15, "OK", "15"]
179
+ # result.results.last # => "15" (new login count)
180
+ #
181
+ # @example Performance optimization for object operations
182
+ # user = User.new(userid: 'user_456')
183
+ #
184
+ # # Instead of multiple round-trips:
185
+ # # user.save # Round-trip 1
186
+ # # user.tags.add('premium') # Round-trip 2
187
+ # # user.sessions.clear # Round-trip 3
188
+ #
189
+ # # Use pipeline for single round-trip:
190
+ # result = user.pipelined do |conn|
191
+ # conn.hmset(user.dbkey, user.to_h_for_storage)
192
+ # conn.sadd(user.tags.dbkey, 'premium')
193
+ # conn.del(user.sessions.dbkey)
194
+ # end
195
+ # # All operations completed in one network round-trip
196
+ #
197
+ # @example Using with object's database context
198
+ # class Session < Familia::Horreum
199
+ # logical_database 3 # Use database 3
200
+ # field :user_id
201
+ # field :expires_at
202
+ # end
203
+ #
204
+ # session = Session.new(session_id: 'sess_789')
205
+ # result = session.pipelined do |conn|
206
+ # # Commands automatically execute in database 3
207
+ # conn.hset(session.dbkey, 'user_id', 'user_123')
208
+ # conn.hset(session.dbkey, 'expires_at', 1.hour.from_now.to_i)
209
+ # conn.expire(session.dbkey, 3600)
210
+ # end
211
+ #
212
+ # @example Reentrant behavior with global pipelines
213
+ # customer = Customer.new(custid: 'cust_abc')
214
+ #
215
+ # # When called within a global pipeline, reuses the pipeline connection
216
+ # result = Familia.pipelined do |global_conn|
217
+ # global_conn.set('global_counter', 0)
218
+ #
219
+ # # This reuses the same pipeline connection
220
+ # customer.pipelined do |local_conn|
221
+ # local_conn.hset(customer.dbkey, 'updated', Time.now.to_i)
222
+ # Redis::Future.new # Returns Redis::Future in nested context
223
+ # end
224
+ # end
225
+ #
226
+ # @note Connection Inheritance:
227
+ # - Uses object's logical_database setting if configured
228
+ # - Inherits class-level database settings
229
+ # - Falls back to instance-level dbclient if set
230
+ # - Uses global connection chain as final fallback
231
+ #
232
+ # @note Pipeline Context:
233
+ # - When called outside global pipeline: Creates local MultiResult
234
+ # - When called inside global pipeline: Yields to existing pipeline
235
+ # - Maintains proper Fiber-local state for nested calls
236
+ #
237
+ # @note Performance Considerations:
238
+ # - Best for multiple independent operations on the same object
239
+ # - Reduces network latency by batching commands
240
+ # - Commands execute independently (some may succeed, others fail)
241
+ #
242
+ # @see Familia.pipelined For global pipeline method
243
+ # @see MultiResult For details on the return value structure
244
+ # @see Familia.transaction For atomic command execution
245
+ def pipelined(&block)
246
+ Familia::Connection::PipelineCore.execute_pipeline(-> { dbclient }, &block)
247
+ end
248
+ alias pipeline pipelined
249
+
250
+ private
251
+
252
+ # Builds the class-level connection chain with handlers in priority order
253
+ def build_connection_chain
254
+ # Cache handlers at class level to avoid creating new instances per model instance
255
+ @fiber_connection_handler ||= Familia::Connection::FiberConnectionHandler.new
256
+ @provider_connection_handler ||= Familia::Connection::ProviderConnectionHandler.new
257
+
258
+ # Determine the appropriate class context
259
+ # When called from instance: self is instance, self.class is the model class
260
+ # When called from class: self is the model class
261
+ klass = self.is_a?(Class) ? self : self.class
262
+
263
+ # Always check class first for @dbclient since instance-level connections were removed
264
+ @cached_connection_handler ||= Familia::Connection::CachedConnectionHandler.new(klass)
265
+ @create_connection_handler ||= Familia::Connection::CreateConnectionHandler.new(klass)
266
+
267
+ Familia::Connection::ResponsibilityChain.new
268
+ .add_handler(Familia::Connection::FiberTransactionHandler.instance)
269
+ .add_handler(@fiber_connection_handler)
270
+ .add_handler(@provider_connection_handler)
271
+ .add_handler(@cached_connection_handler)
272
+ .add_handler(@create_connection_handler)
70
273
  end
71
274
  end
72
275
  end
@@ -1,13 +1,13 @@
1
1
  # lib/familia/horreum/database_commands.rb
2
2
 
3
3
  module Familia
4
- # InstanceMethods - Module containing instance-level methods for Familia
4
+ # Familia::Horreum
5
5
  #
6
6
  # This module is included in classes that include Familia, providing
7
7
  # instance-level functionality for Database operations and object management.
8
8
  #
9
9
  class Horreum
10
- # Methods that call Database commands (InstanceMethods)
10
+ # DatabaseCommands - Instance-level methods for horreum models that call Database commands
11
11
  #
12
12
  # NOTE: There is no hgetall for Horreum. This is because Horreum
13
13
  # is a single hash in Database that we aren't meant to have be working
@@ -20,11 +20,11 @@ module Familia
20
20
  dbclient.move dbkey, logical_database
21
21
  end
22
22
 
23
- # Checks if the calling object's key exists in Redis.
23
+ # Checks if the calling object's key exists in the database.
24
24
  #
25
25
  # @param check_size [Boolean] When true (default), also verifies the hash has a non-zero size.
26
26
  # When false, only checks key existence regardless of content.
27
- # @return [Boolean] Returns `true` if the key exists in Redis. When `check_size` is true,
27
+ # @return [Boolean] Returns `true` if the key exists in the database. When `check_size` is true,
28
28
  # also requires the hash to have at least one field.
29
29
  #
30
30
  # @example Check existence with size validation (default behavior)
@@ -49,6 +49,7 @@ module Familia
49
49
  dbclient.hlen dbkey
50
50
  end
51
51
  alias size field_count
52
+ alias length field_count
52
53
 
53
54
  # Sets a timeout on key. After the timeout has expired, the key will
54
55
  # automatically be deleted. Returns 1 if the timeout was set, 0 if key
@@ -56,19 +57,19 @@ module Familia
56
57
  #
57
58
  def expire(default_expiration = nil)
58
59
  default_expiration ||= self.class.default_expiration
59
- Familia.trace :EXPIRE, dbclient, default_expiration, caller(1..1) if Familia.debug?
60
+ Familia.trace :EXPIRE, nil, default_expiration if Familia.debug?
60
61
  dbclient.expire dbkey, default_expiration.to_i
61
62
  end
62
63
 
63
64
  # Retrieves the remaining time to live (TTL) for the object's dbkey.
64
65
  #
65
- # This method accesses the ovjects Database client to obtain the TTL of `dbkey`.
66
+ # This method accesses the objects Database client to obtain the TTL of `dbkey`.
66
67
  # If debugging is enabled, it logs the TTL retrieval operation using `Familia.trace`.
67
68
  #
68
69
  # @return [Integer] The TTL of the key in seconds. Returns -1 if the key does not exist
69
70
  # or has no associated expire time.
70
71
  def current_expiration
71
- Familia.trace :CURRENT_EXPIRATION, dbclient, uri, caller(1..1) if Familia.debug?
72
+ Familia.trace :CURRENT_EXPIRATION, nil, uri if Familia.debug?
72
73
  dbclient.ttl dbkey
73
74
  end
74
75
 
@@ -77,32 +78,32 @@ module Familia
77
78
  # @param field [String] The field to remove from the hash.
78
79
  # @return [Integer] The number of fields that were removed from the hash (0 or 1).
79
80
  def remove_field(field)
80
- Familia.trace :HDEL, dbclient, field, caller(1..1) if Familia.debug?
81
+ Familia.trace :HDEL, nil, field if Familia.debug?
81
82
  dbclient.hdel dbkey, field
82
83
  end
83
84
  alias remove remove_field # deprecated
84
85
 
85
86
  def data_type
86
- Familia.trace :DATATYPE, dbclient, uri, caller(1..1) if Familia.debug?
87
+ Familia.trace :DATATYPE, nil, uri if Familia.debug?
87
88
  dbclient.type dbkey(suffix)
88
89
  end
89
90
 
90
91
  # For parity with DataType#hgetall
91
92
  def hgetall
92
- Familia.trace :HGETALL, dbclient, uri, caller(1..1) if Familia.debug?
93
+ Familia.trace :HGETALL, nil, uri if Familia.debug?
93
94
  dbclient.hgetall dbkey(suffix)
94
95
  end
95
96
  alias all hgetall
96
97
 
97
98
  def hget(field)
98
- Familia.trace :HGET, dbclient, field, caller(1..1) if Familia.debug?
99
+ Familia.trace :HGET, nil, field if Familia.debug?
99
100
  dbclient.hget dbkey(suffix), field
100
101
  end
101
102
 
102
103
  # @return The number of fields that were added to the hash. If the
103
104
  # field already exists, this will return 0.
104
105
  def hset(field, value)
105
- Familia.trace :HSET, dbclient, field, caller(1..1) if Familia.debug?
106
+ Familia.trace :HSET, nil, field if Familia.debug?
106
107
  dbclient.hset dbkey, field, value
107
108
  end
108
109
 
@@ -115,18 +116,18 @@ module Familia
115
116
  # @return [Integer] 1 if the field is a new field in the hash and the value was set,
116
117
  # 0 if the field already exists in the hash and no operation was performed
117
118
  def hsetnx(field, value)
118
- Familia.trace :HSETNX, dbclient, field, caller(1..1) if Familia.debug?
119
+ Familia.trace :HSETNX, nil, field if Familia.debug?
119
120
  dbclient.hsetnx dbkey, field, value
120
121
  end
121
122
 
122
123
  def hmset(hsh = {})
123
124
  hsh ||= to_h
124
- Familia.trace :HMSET, dbclient, hsh, caller(1..1) if Familia.debug?
125
+ Familia.trace :HMSET, nil, hsh if Familia.debug?
125
126
  dbclient.hmset dbkey(suffix), hsh
126
127
  end
127
128
 
128
129
  def hkeys
129
- Familia.trace :HKEYS, dbclient, 'uri', caller(1..1) if Familia.debug?
130
+ Familia.trace :HKEYS, nil, 'uri' if Familia.debug?
130
131
  dbclient.hkeys dbkey(suffix)
131
132
  end
132
133
 
@@ -169,14 +170,23 @@ module Familia
169
170
  end
170
171
  alias has_key? key?
171
172
 
172
- # Deletes the entire dbkey
173
+ # Deletes the dbkey for this horreum :object.
174
+ #
175
+ # It does not delete the related fields keys. See destroy!
176
+ #
173
177
  # @return [Boolean] true if the key was deleted, false otherwise
174
178
  def delete!
175
- Familia.trace :DELETE!, dbclient, uri, caller(1..1) if Familia.debug?
179
+ Familia.trace :DELETE!, nil, uri if Familia.debug?
180
+
181
+ # Delete the main object key
176
182
  ret = dbclient.del dbkey
177
183
  ret.positive?
178
184
  end
179
185
  alias clear delete!
186
+
187
+ def echo(*args)
188
+ dbclient.echo "[#{self.class}] #{args.join(' ')}"
189
+ end
180
190
  end
181
191
  end
182
192
  end
@@ -1,5 +1,5 @@
1
1
  # lib/familia/horreum/serialization.rb
2
- #
2
+
3
3
  module Familia
4
4
  # Familia::Horreum
5
5
  #
@@ -21,7 +21,7 @@ module Familia
21
21
  # - nil - Valid response for certain operations
22
22
  #
23
23
  # @example Validating a command response
24
- # response = redis.set("key", "value")
24
+ # response = dbclient.set("key", "value")
25
25
  # valid = @valid_command_return_values.include?(response)
26
26
  # # => true if response is "OK"
27
27
  #
@@ -31,7 +31,7 @@ module Familia
31
31
  attr_reader :valid_command_return_values
32
32
  end
33
33
 
34
- # Serialization: Object persistence and retrieval from the DB
34
+ # Serialization - Instance-level methods for object persistence and retrieval
35
35
  # Handles conversion between Ruby objects and Valkey hash storage
36
36
  #
37
37
  module Serialization
@@ -61,7 +61,7 @@ module Familia
61
61
  # @see #commit_fields The underlying method that performs the field persistence
62
62
  #
63
63
  def save(update_expiration: true)
64
- Familia.trace :SAVE, dbclient, uri, caller(1..1) if Familia.debug?
64
+ Familia.trace :SAVE, nil, uri if Familia.debug?
65
65
 
66
66
  # No longer need to sync computed identifier with a cache field
67
67
  self.created ||= Familia.now.to_i if respond_to?(:created)
@@ -71,6 +71,9 @@ module Familia
71
71
  #
72
72
  ret = commit_fields(update_expiration: update_expiration)
73
73
 
74
+ # Add to class-level instances collection after successful save
75
+ self.class.instances.add(identifier, Familia.now) if ret && self.class.respond_to?(:instances)
76
+
74
77
  Familia.ld "[save] #{self.class} #{dbkey} #{ret} (update_expiration: #{update_expiration})"
75
78
 
76
79
  # Did Database accept our offering?
@@ -123,7 +126,7 @@ module Familia
123
126
  identifier_field = self.class.identifier_field
124
127
 
125
128
  Familia.ld "[save_if_not_exists]: #{self.class} #{identifier_field}=#{identifier}"
126
- Familia.trace :SAVE_IF_NOT_EXISTS, dbclient, uri, caller(1..1) if Familia.debug?
129
+ Familia.trace :SAVE_IF_NOT_EXISTS, nil, uri if Familia.debug?
127
130
 
128
131
  dbclient.watch(dbkey) do
129
132
  if dbclient.exists(dbkey).positive?
@@ -180,7 +183,7 @@ module Familia
180
183
 
181
184
  # Updates multiple fields atomically in a Database transaction.
182
185
  #
183
- # @param fields [Hash] Field names and values to update. Special key :update_expiration
186
+ # @param kwargs [Hash] Field names and values to update. Special key :update_expiration
184
187
  # controls whether to update key expiration (default: true)
185
188
  # @return [MultiResult] Transaction result
186
189
  #
@@ -194,9 +197,9 @@ module Familia
194
197
  update_expiration = kwargs.delete(:update_expiration) { true }
195
198
  fields = kwargs
196
199
 
197
- Familia.trace :BATCH_UPDATE, dbclient, fields.keys, caller(1..1) if Familia.debug?
200
+ Familia.trace :BATCH_UPDATE, nil, fields.keys if Familia.debug?
198
201
 
199
- command_return_values = transaction do |conn|
202
+ transaction_result = transaction do |conn|
200
203
  fields.each do |field, value|
201
204
  prepared_value = serialize_value(value)
202
205
  conn.hset dbkey, field, prepared_value
@@ -208,9 +211,8 @@ module Familia
208
211
  # Update expiration if requested and supported
209
212
  self.update_expiration(default_expiration: nil) if update_expiration && respond_to?(:update_expiration)
210
213
 
211
- # Return same MultiResult format as other methods
212
- summary_boolean = command_return_values.all? { |ret| %w[OK 0 1].include?(ret.to_s) }
213
- MultiResult.new(summary_boolean, command_return_values)
214
+ # Return the MultiResult directly (transaction already returns MultiResult)
215
+ transaction_result
214
216
  end
215
217
 
216
218
  # Updates the object by applying multiple field values.
@@ -235,11 +237,12 @@ module Familia
235
237
  self
236
238
  end
237
239
 
238
- # Permanently removes this object from the DB storage.
240
+ # Permanently removes this object and its related fields from the DB.
239
241
  #
240
- # Deletes the object's Valkey key and all associated data. This operation
242
+ # Deletes the object's database key and all associated data. This operation
241
243
  # is irreversible and will permanently destroy all stored information
242
- # for this object instance.
244
+ # for this object instance and the additional list, set, hash, string
245
+ # etc fields defined for this class.
243
246
  #
244
247
  # @return [void]
245
248
  #
@@ -259,8 +262,25 @@ module Familia
259
262
  # @see #delete! The underlying method that performs the key deletion
260
263
  #
261
264
  def destroy!
262
- Familia.trace :DESTROY, dbclient, uri, caller(1..1) if Familia.debug?
263
- delete!
265
+ Familia.trace :DESTROY, dbkey, uri
266
+
267
+ # Execute all deletion operations within a transaction
268
+ transaction do |conn|
269
+ # Delete the main object key
270
+ conn.del(dbkey)
271
+
272
+ # Delete all related fields if present
273
+ if self.class.relations?
274
+ Familia.trace :DELETE_RELATED_FIELDS!, nil,
275
+ "#{self.class} has relations: #{self.class.related_fields.keys}"
276
+
277
+ self.class.related_fields.each do |name, _definition|
278
+ obj = send(name)
279
+ Familia.trace :DELETE_RELATED_FIELD, name, "Deleting related field #{name} (#{obj.dbkey})"
280
+ conn.del(obj.dbkey)
281
+ end
282
+ end
283
+ end
264
284
  end
265
285
 
266
286
  # Clears all fields by setting them to nil.
@@ -306,7 +326,7 @@ module Familia
306
326
  # no authoritative source in Valkey storage.
307
327
  #
308
328
  def refresh!
309
- Familia.trace :REFRESH, dbclient, uri, caller(1..1) if Familia.debug?
329
+ Familia.trace :REFRESH, nil, uri if Familia.debug?
310
330
  raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
311
331
 
312
332
  fields = hgetall
@@ -442,7 +462,7 @@ module Familia
442
462
  #
443
463
  # The serialization process:
444
464
  # 1. Attempts conversion using Familia.distinguisher with relaxed type checking
445
- # 2. For Hash/Array types that return nil, tries custom dump_method or JSON.dump
465
+ # 2. For Hash/Array types that return nil, tries custom dump_method or Familia::JsonSerializer.dump
446
466
  # 3. Logs warnings when serialization fails completely
447
467
  #
448
468
  # @param val [Object] The Ruby object to serialize for Valkey storage
@@ -485,7 +505,7 @@ module Familia
485
505
  # Hash or Array types. Simple string values are returned as-is.
486
506
  #
487
507
  # @param val [String] The string value from Database to deserialize
488
- # @param symbolize_keys [Boolean] Whether to symbolize hash keys (default: true for compatibility)
508
+ # @param symbolize [Boolean] Whether to symbolize hash keys (default: true for compatibility)
489
509
  # @return [Object] The deserialized value (Hash, Array, or original string)
490
510
  #
491
511
  def deserialize_value(val, symbolize: true)
@@ -522,7 +542,7 @@ module Familia
522
542
  field_type = self.class.field_types[field_name]
523
543
  next unless field_type&.method_name
524
544
 
525
- # Set the transient field back to nil
545
+ # UnsortedSet the transient field back to nil
526
546
  send("#{field_type.method_name}=", nil)
527
547
  Familia.ld "[reset_transient_fields!] Reset #{field_name} to nil"
528
548
  end
@@ -7,7 +7,8 @@ module Familia
7
7
  # instance-level functionality for Database operations and object management.
8
8
  #
9
9
  class Horreum
10
- # Utils - Module containing utility methods for Familia::Horreum (InstanceMethods)
10
+ # Utils - Instance-level utility methods for Familia::Horreum
11
+ # Provides identifier handling, dbkey generation, and object inspection
11
12
  #
12
13
  module Utils
13
14
  # def uri
@@ -7,7 +7,8 @@ module Familia
7
7
  # instance-level functionality for Database operations and object management.
8
8
  #
9
9
  class Horreum
10
- # Settings - Module containing settings for Familia::Horreum (InstanceMethods)
10
+ # Settings - Instance-level configuration methods for Horreum models
11
+ # Provides per-instance settings like logical_database, dump_method, load_method, suffix
11
12
  #
12
13
  module Settings
13
14
  attr_writer :dump_method, :load_method, :suffix