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,5 +1,5 @@
1
- # lib/familia/horreum/serialization.rb
2
- #
1
+ # lib/familia/horreum/persistence.rb
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,10 +31,10 @@ 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
- module Serialization
37
+ module Persistence
38
38
  # Persists the object to Valkey storage with automatic timestamping.
39
39
  #
40
40
  # Saves the current object state to Valkey storage, automatically setting
@@ -61,16 +61,26 @@ 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)
68
68
  self.updated = Familia.now.to_i if respond_to?(:updated)
69
69
 
70
70
  # Commit our tale to the Database chronicles
71
- #
71
+ # Wrap in transaction for atomicity between save and indexing
72
72
  ret = commit_fields(update_expiration: update_expiration)
73
73
 
74
+ # Auto-index for class-level indexes after successful save
75
+ # Use transaction to ensure atomicity with the save operation
76
+ if ret
77
+ transaction do |conn|
78
+ auto_update_class_indexes
79
+ # Add to class-level instances collection after successful save
80
+ self.class.instances.add(identifier, Familia.now) if self.class.respond_to?(:instances)
81
+ end
82
+ end
83
+
74
84
  Familia.ld "[save] #{self.class} #{dbkey} #{ret} (update_expiration: #{update_expiration})"
75
85
 
76
86
  # Did Database accept our offering?
@@ -123,9 +133,9 @@ module Familia
123
133
  identifier_field = self.class.identifier_field
124
134
 
125
135
  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?
136
+ Familia.trace :SAVE_IF_NOT_EXISTS, nil, uri if Familia.debug?
127
137
 
128
- dbclient.watch(dbkey) do
138
+ success = dbclient.watch(dbkey) do
129
139
  if dbclient.exists(dbkey).positive?
130
140
  dbclient.unwatch
131
141
  raise Familia::RecordExistsError, dbkey
@@ -137,6 +147,16 @@ module Familia
137
147
 
138
148
  result.is_a?(Array) # transaction succeeded
139
149
  end
150
+
151
+ # Auto-index for class-level indexes after successful save
152
+ # Use transaction to ensure atomicity with the save operation
153
+ if success
154
+ transaction do |conn|
155
+ auto_update_class_indexes
156
+ end
157
+ end
158
+
159
+ success
140
160
  end
141
161
 
142
162
  # Commits object fields to the DB storage.
@@ -180,7 +200,7 @@ module Familia
180
200
 
181
201
  # Updates multiple fields atomically in a Database transaction.
182
202
  #
183
- # @param fields [Hash] Field names and values to update. Special key :update_expiration
203
+ # @param kwargs [Hash] Field names and values to update. Special key :update_expiration
184
204
  # controls whether to update key expiration (default: true)
185
205
  # @return [MultiResult] Transaction result
186
206
  #
@@ -194,9 +214,9 @@ module Familia
194
214
  update_expiration = kwargs.delete(:update_expiration) { true }
195
215
  fields = kwargs
196
216
 
197
- Familia.trace :BATCH_UPDATE, dbclient, fields.keys, caller(1..1) if Familia.debug?
217
+ Familia.trace :BATCH_UPDATE, nil, fields.keys if Familia.debug?
198
218
 
199
- command_return_values = transaction do |conn|
219
+ transaction_result = transaction do |conn|
200
220
  fields.each do |field, value|
201
221
  prepared_value = serialize_value(value)
202
222
  conn.hset dbkey, field, prepared_value
@@ -208,9 +228,8 @@ module Familia
208
228
  # Update expiration if requested and supported
209
229
  self.update_expiration(default_expiration: nil) if update_expiration && respond_to?(:update_expiration)
210
230
 
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)
231
+ # Return the MultiResult directly (transaction already returns MultiResult)
232
+ transaction_result
214
233
  end
215
234
 
216
235
  # Updates the object by applying multiple field values.
@@ -235,11 +254,12 @@ module Familia
235
254
  self
236
255
  end
237
256
 
238
- # Permanently removes this object from the DB storage.
257
+ # Permanently removes this object and its related fields from the DB.
239
258
  #
240
- # Deletes the object's Valkey key and all associated data. This operation
259
+ # Deletes the object's database key and all associated data. This operation
241
260
  # is irreversible and will permanently destroy all stored information
242
- # for this object instance.
261
+ # for this object instance and the additional list, set, hash, string
262
+ # etc fields defined for this class.
243
263
  #
244
264
  # @return [void]
245
265
  #
@@ -259,8 +279,25 @@ module Familia
259
279
  # @see #delete! The underlying method that performs the key deletion
260
280
  #
261
281
  def destroy!
262
- Familia.trace :DESTROY, dbclient, uri, caller(1..1) if Familia.debug?
263
- delete!
282
+ Familia.trace :DESTROY, dbkey, uri
283
+
284
+ # Execute all deletion operations within a transaction
285
+ transaction do |conn|
286
+ # Delete the main object key
287
+ conn.del(dbkey)
288
+
289
+ # Delete all related fields if present
290
+ if self.class.relations?
291
+ Familia.trace :DELETE_RELATED_FIELDS!, nil,
292
+ "#{self.class} has relations: #{self.class.related_fields.keys}"
293
+
294
+ self.class.related_fields.each do |name, _definition|
295
+ obj = send(name)
296
+ Familia.trace :DELETE_RELATED_FIELD, name, "Deleting related field #{name} (#{obj.dbkey})"
297
+ conn.del(obj.dbkey)
298
+ end
299
+ end
300
+ end
264
301
  end
265
302
 
266
303
  # Clears all fields by setting them to nil.
@@ -306,7 +343,7 @@ module Familia
306
343
  # no authoritative source in Valkey storage.
307
344
  #
308
345
  def refresh!
309
- Familia.trace :REFRESH, dbclient, uri, caller(1..1) if Familia.debug?
346
+ Familia.trace :REFRESH, nil, uri if Familia.debug?
310
347
  raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
311
348
 
312
349
  fields = hgetall
@@ -341,169 +378,6 @@ module Familia
341
378
  self
342
379
  end
343
380
 
344
- # Converts the object's persistent fields to a hash for external use.
345
- #
346
- # Serializes persistent field values for external consumption (APIs, logs),
347
- # excluding non-loggable fields like encrypted fields for security.
348
- # Only non-nil values are included in the resulting hash.
349
- #
350
- # @return [Hash] Hash with field names as keys and serialized values
351
- # safe for external exposure
352
- #
353
- # @example Converting an object to hash format for API response
354
- # user = User.new(name: "John", email: "john@example.com", age: 30)
355
- # user.to_h
356
- # # => {"name"=>"John", "email"=>"john@example.com", "age"=>"30"}
357
- # # encrypted fields are excluded for security
358
- #
359
- # @note Only loggable fields are included for security
360
- # @note Only fields with non-nil values are included
361
- #
362
- def to_h
363
- self.class.persistent_fields.each_with_object({}) do |field, hsh|
364
- field_type = self.class.field_types[field]
365
-
366
- # Security: Skip non-loggable fields (e.g., encrypted fields)
367
- next unless field_type.loggable
368
-
369
- method_name = field_type.method_name
370
- val = send(method_name)
371
- prepared = serialize_value(val)
372
- Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
373
-
374
- # Only include non-nil values in the hash for Valkey
375
- # Use string key for database compatibility
376
- hsh[field.to_s] = prepared unless prepared.nil?
377
- end
378
- end
379
-
380
- # Converts the object's persistent fields to a hash for database storage.
381
- #
382
- # Serializes ALL persistent field values for database storage, including
383
- # encrypted fields. This is used internally by commit_fields and other
384
- # persistence operations.
385
- #
386
- # @return [Hash] Hash with field names as keys and serialized values
387
- # ready for database storage
388
- #
389
- # @note Includes ALL persistent fields, including encrypted fields
390
- # @note Only fields with non-nil values are included for storage efficiency
391
- #
392
- def to_h_for_storage
393
- self.class.persistent_fields.each_with_object({}) do |field, hsh|
394
- field_type = self.class.field_types[field]
395
- method_name = field_type.method_name
396
- val = send(method_name)
397
- prepared = serialize_value(val)
398
- Familia.ld " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
399
-
400
- # Only include non-nil values in the hash for Valkey
401
- # Use string key for database compatibility
402
- hsh[field.to_s] = prepared unless prepared.nil?
403
- end
404
- end
405
-
406
- # Converts the object's persistent fields to an array.
407
- #
408
- # Serializes all persistent field values in field definition order,
409
- # preparing them for Valkey storage. Each value is processed through
410
- # the serialization pipeline to ensure Valkey compatibility.
411
- #
412
- # @return [Array] Array of serialized field values in field order
413
- #
414
- # @example Converting an object to array format
415
- # user = User.new(name: "John", email: "john@example.com", age: 30)
416
- # user.to_a
417
- # # => ["John", "john@example.com", "30"]
418
- #
419
- # @note Values are serialized using the same process as other persistence
420
- # methods to maintain data consistency across operations.
421
- #
422
- def to_a
423
- self.class.persistent_fields.filter_map do |field|
424
- field_type = self.class.field_types[field]
425
-
426
- # Security: Skip non-loggable fields (e.g., encrypted fields)
427
- next unless field_type.loggable
428
-
429
- method_name = field_type.method_name
430
- val = send(method_name)
431
- prepared = serialize_value(val)
432
- Familia.ld " [to_a] field: #{field} method: #{method_name} val: #{val.class} prepared: #{prepared.class}"
433
- prepared
434
- end
435
- end
436
-
437
- # Serializes a Ruby object for Valkey storage.
438
- #
439
- # Converts Ruby objects into the DB-compatible string representations using
440
- # the Familia distinguisher for type coercion. Falls back to JSON serialization
441
- # for complex types (Hash, Array) when the primary distinguisher returns nil.
442
- #
443
- # The serialization process:
444
- # 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
446
- # 3. Logs warnings when serialization fails completely
447
- #
448
- # @param val [Object] The Ruby object to serialize for Valkey storage
449
- #
450
- # @return [String, nil] The serialized value ready for Valkey storage, or nil
451
- # if serialization failed
452
- #
453
- # @example Serializing different data types
454
- # serialize_value("hello") # => "hello"
455
- # serialize_value(42) # => "42"
456
- # serialize_value({name: "John"}) # => '{"name":"John"}'
457
- # serialize_value([1, 2, 3]) # => "[1,2,3]"
458
- #
459
- # @note This method integrates with Familia's type system and supports
460
- # custom serialization methods when available on the object
461
- #
462
- # @see Familia.distinguisher The primary serialization mechanism
463
- #
464
- def serialize_value(val)
465
- # Security: Handle ConcealedString safely - extract encrypted data for storage
466
- return val.encrypted_value if val.respond_to?(:encrypted_value)
467
-
468
- prepared = Familia.distinguisher(val, strict_values: false)
469
-
470
- # If the distinguisher returns nil, try using the dump_method but only
471
- # use JSON serialization for complex types that need it.
472
- if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
473
- prepared = val.respond_to?(dump_method) ? val.send(dump_method) : Familia::JsonSerializer.dump(val)
474
- end
475
-
476
- # If both the distinguisher and dump_method return nil, log an error
477
- Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}" if prepared.nil?
478
-
479
- prepared
480
- end
481
-
482
- # Converts a Database string value back to its original Ruby type
483
- #
484
- # This method attempts to deserialize JSON strings back to their original
485
- # Hash or Array types. Simple string values are returned as-is.
486
- #
487
- # @param val [String] The string value from Database to deserialize
488
- # @param symbolize_keys [Boolean] Whether to symbolize hash keys (default: true for compatibility)
489
- # @return [Object] The deserialized value (Hash, Array, or original string)
490
- #
491
- def deserialize_value(val, symbolize: true)
492
- return val if val.nil? || val == ''
493
-
494
- # Try to parse as JSON first for complex types
495
- begin
496
- parsed = Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
497
- # Only return parsed value if it's a complex type (Hash/Array)
498
- # Simple values should remain as strings
499
- return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
500
- rescue Familia::SerializerError
501
- # Not valid JSON, return as-is
502
- end
503
-
504
- val
505
- end
506
-
507
381
  private
508
382
 
509
383
  # Reset all transient fields to nil
@@ -522,11 +396,60 @@ module Familia
522
396
  field_type = self.class.field_types[field_name]
523
397
  next unless field_type&.method_name
524
398
 
525
- # Set the transient field back to nil
399
+ # UnsortedSet the transient field back to nil
526
400
  send("#{field_type.method_name}=", nil)
527
401
  Familia.ld "[reset_transient_fields!] Reset #{field_name} to nil"
528
402
  end
529
403
  end
404
+
405
+ # Automatically update class-level indexes after save
406
+ #
407
+ # Iterates through class-level indexing relationships and calls their
408
+ # corresponding add_to_class_* methods to populate indexes. Only processes
409
+ # class-level indexes (where target_class == self.class), skipping
410
+ # instance-scoped indexes which require parent context.
411
+ #
412
+ # Uses idempotent Redis commands (HSET for unique_index) so repeated calls
413
+ # are safe and have negligible performance overhead. Note that multi_index
414
+ # always requires within: parameter, so only unique_index benefits from this.
415
+ #
416
+ # @return [void]
417
+ #
418
+ # @example Automatic indexing on save
419
+ # class Customer < Familia::Horreum
420
+ # feature :relationships
421
+ # unique_index :email, :email_lookup
422
+ # end
423
+ #
424
+ # customer = Customer.new(email: 'test@example.com')
425
+ # customer.save # Automatically calls add_to_class_email_lookup
426
+ #
427
+ # @note Only class-level unique_index declarations auto-populate.
428
+ # Instance-scoped indexes (with within:) require manual population:
429
+ # employee.add_to_company_badge_index(company)
430
+ #
431
+ # @see Familia::Features::Relationships::Indexing For index declaration details
432
+ #
433
+ def auto_update_class_indexes
434
+ return unless self.class.respond_to?(:indexing_relationships)
435
+
436
+ self.class.indexing_relationships.each do |rel|
437
+ # Skip instance-scoped indexes (require parent context)
438
+ # Instance-scoped indexes must be manually populated because they need
439
+ # the parent object reference (e.g., employee.add_to_company_badge_index(company))
440
+ unless rel.target_class == self.class
441
+ Familia.ld <<~LOG_MESSAGE
442
+ [auto_update_class_indexes] Skipping #{rel.index_name} (requires parent context)
443
+ LOG_MESSAGE
444
+ next
445
+ end
446
+
447
+ # Call the existing add_to_class_* methods
448
+ add_method = :"add_to_class_#{rel.index_name}"
449
+ send(add_method) if respond_to?(add_method)
450
+ end
451
+ end
452
+
530
453
  end
531
454
  end
532
455
  end
@@ -1,32 +1,99 @@
1
- # lib/familia/horreum/related_fields_management.rb
1
+ # lib/familia/horreum/related_fields.rb
2
2
 
3
3
  module Familia
4
+
5
+ RelatedFieldDefinition = Data.define(:name, :klass, :opts)
6
+
4
7
  class Horreum
8
+
9
+ # Each related field needs some details from the parent (Horreum model)
10
+ # in order to generate its dbkey. We use a parent proxy pattern to store
11
+ # only essential parent information instead of full object reference. We
12
+ # need only the model class and an optional unique identifier to generate
13
+ # the dbkey; when the identifier is nil, we treat this as a class-level
14
+ # relation (e.g. model_name:related_field_name); when the identifier
15
+ # is not nil, we treat this as an instance-level relation
16
+ # (model_name:identifier:related_field_name).
17
+ #
18
+ ParentDefinition = Data.define(:model_klass, :identifier) do
19
+ # Factory method to create ParentDefinition from a parent instance
20
+ def self.from_parent(parent_instance)
21
+ case parent_instance
22
+ when Class
23
+ # Handle class-level relationships
24
+ new(parent_instance, nil)
25
+ else
26
+ # Handle instance-level relationships
27
+ identifier = parent_instance.respond_to?(:identifier) ? parent_instance.identifier : nil
28
+ new(parent_instance.class, identifier)
29
+ end
30
+ end
31
+
32
+ # Delegation methods for common operations needed by DataTypes
33
+ def dbclient(uri = nil)
34
+ model_klass.dbclient(uri)
35
+ end
36
+
37
+ def logical_database
38
+ model_klass.logical_database
39
+ end
40
+
41
+ def dbkey(keystring = nil)
42
+ if identifier
43
+ # Instance-level relation: model_name:identifier:keystring
44
+ model_klass.dbkey(identifier, keystring)
45
+ else
46
+ # Class-level relation: model_name:keystring
47
+ model_klass.dbkey(keystring, nil)
48
+ end
49
+ end
50
+
51
+ # Allow comparison with the original parent instance
52
+ def ==(other)
53
+ case other
54
+ when ParentDefinition
55
+ model_klass == other.model_klass && identifier == other.identifier
56
+ when Class
57
+ model_klass == other && identifier.nil?
58
+ else
59
+ # Compare with instance: check class and identifier match
60
+ other.is_a?(model_klass) && other.respond_to?(:identifier) && identifier == other.identifier
61
+ end
62
+ end
63
+ alias eql? ==
64
+ end
65
+
66
+ # RelatedFieldsManagement - Class-level methods for defining DataType relationships
5
67
  #
6
- # RelatedFieldsManagement: Manages DataType fields and relations
68
+ # This module uses metaprogramming to dynamically create field definition methods
69
+ # that generate both class-level and instance-level accessor methods for DataTypes
70
+ # (e.g., list, set, zset, hashkey, string).
7
71
  #
8
- # This module uses metaprogramming to dynamically create methods
9
- # for managing different types of Database objects (e.g., sets, lists, hashes).
72
+ # When included in a class via ManagementMethods, it provides class methods like:
73
+ # * Customer.list :recent_orders # defines class method for class-level list
74
+ # * customer.recent_orders # creates instance method returning list instance
10
75
  #
11
76
  # Key metaprogramming features:
12
- # * Dynamically defines methods for each Database type (e.g., set, list, hashkey)
13
- # * Creates both instance-level and class-level relation methods
77
+ # * Dynamically defines DSL methods for each Database type (e.g., set, list, hashkey)
78
+ # * Each DSL method creates corresponding instance/class accessor methods
14
79
  # * Provides query methods for checking relation types
15
80
  #
16
81
  # Usage:
17
82
  # Include this module in classes that need DataType management
18
- # Call setup_related_fields_accessors to initialize the feature
83
+ # Call setup_related_fields_definition_methods to initialize the feature
19
84
  #
20
85
  module RelatedFieldsManagement
21
86
  # A practical flag to indicate that a Horreum member has relations,
22
87
  # not just theoretically but actually at least one list/haskey/etc.
23
- @has_relations = nil
88
+ @has_related_fields = nil
24
89
 
25
90
  def self.included(base)
26
91
  base.extend(RelatedFieldsAccessors)
27
- base.setup_related_fields_accessors
92
+ base.setup_related_fields_definition_methods
28
93
  end
29
94
 
95
+ # RelatedFieldsManagement::RelatedFieldsAccessors
96
+ #
30
97
  module RelatedFieldsAccessors
31
98
  # Sets up all DataType related methods
32
99
  # This method generates the following for each registered DataType:
@@ -36,9 +103,9 @@ module Familia
36
103
  # Collection methods: sets(), lists(), hashkeys(), sorted_sets(), etc.
37
104
  # Class methods: class_set(), class_list(), etc.
38
105
  #
39
- def setup_related_fields_accessors
106
+ def setup_related_fields_definition_methods
40
107
  Familia::DataType.registered_types.each_pair do |kind, klass|
41
- Familia.trace :registered_types, kind, klass, caller(1..1) if Familia.debug?
108
+ Familia.trace :registered_types, kind, klass if Familia.debug?
42
109
 
43
110
  # Dynamically define instance-level relation methods
44
111
  #
@@ -50,7 +117,7 @@ module Familia
50
117
  name, opts = *args
51
118
 
52
119
  # As log as we have at least one relation, we can set this flag.
53
- @has_relations = true
120
+ @has_related_fields = true
54
121
 
55
122
  attach_instance_related_field name, klass, opts
56
123
  end
@@ -95,18 +162,35 @@ module Familia
95
162
 
96
163
  # Creates an instance-level relation
97
164
  def attach_instance_related_field(name, klass, opts)
98
- Familia.trace :attach_instance, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
165
+ Familia.trace :attach_instance_related_field, name, klass, opts if Familia.debug?
99
166
  raise ArgumentError, "Name is blank (#{klass})" if name.to_s.empty?
100
167
 
101
168
  name = name.to_s.to_sym
102
169
  opts ||= {}
103
170
 
104
- related_fields[name] = Struct.new(:name, :klass, :opts).new
105
- related_fields[name].name = name
106
- related_fields[name].klass = klass
107
- related_fields[name].opts = opts
171
+ related_fields[name] = RelatedFieldDefinition.new(name, klass, opts)
108
172
 
109
- attr_reader name
173
+ # Create lazy-initializing accessor that calls initialize_relatives if needed
174
+ define_method name do
175
+ ivar = :"@#{name}"
176
+ value = instance_variable_get(ivar)
177
+
178
+ # If nil and we haven't initialized relatives, do it now
179
+ # Check singleton class to avoid polluting instance variables
180
+ if value.nil? && !singleton_class.instance_variable_defined?(:"@relatives_initialized")
181
+ initialize_relatives
182
+ value = instance_variable_get(ivar)
183
+ end
184
+
185
+ # If still nil after lazy initialization attempt, raise helpful error
186
+ # Only raise if we tried to initialize but it's still nil
187
+ if value.nil? && singleton_class.instance_variable_defined?(:"@relatives_initialized")
188
+ raise "#{self.class}##{name} is nil. Did you override initialize without calling super? " \
189
+ "(Field is nil after initialization attempt)"
190
+ end
191
+
192
+ value
193
+ end
110
194
 
111
195
  define_method :"#{name}=" do |val|
112
196
  send(name).replace val
@@ -120,17 +204,14 @@ module Familia
120
204
 
121
205
  # Creates a class-level relation
122
206
  def attach_class_related_field(name, klass, opts)
123
- Familia.trace :attach_class_related_field, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
207
+ Familia.trace :attach_class_related_field, "#{name} #{klass}", opts if Familia.debug?
124
208
  raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty?
125
209
 
126
210
  name = name.to_s.to_sym
127
211
  opts = opts.nil? ? {} : opts.clone
128
212
  opts[:parent] = self unless opts.key?(:parent)
129
213
 
130
- class_related_fields[name] = Struct.new(:name, :klass, :opts).new
131
- class_related_fields[name].name = name
132
- class_related_fields[name].klass = klass
133
- class_related_fields[name].opts = opts
214
+ class_related_fields[name] = RelatedFieldDefinition.new(name, klass, opts)
134
215
 
135
216
  # An accessor method created in the metaclass will
136
217
  # access the instance variables for this class.