familia 2.0.0.pre15 → 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 (274) 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 +64 -4
  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 +2 -2
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre5.md +33 -12
  42. data/docs/migrating/v2.0.0-pre6.md +2 -2
  43. data/docs/migrating/v2.0.0-pre7.md +8 -8
  44. data/docs/overview.md +623 -19
  45. data/docs/reference/api-technical.md +1365 -0
  46. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  47. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  48. data/examples/autoloader/mega_customer.rb +3 -1
  49. data/examples/encrypted_fields.rb +378 -0
  50. data/examples/json_usage_patterns.rb +144 -0
  51. data/examples/relationships.rb +13 -13
  52. data/examples/safe_dump.rb +6 -6
  53. data/examples/single_connection_transaction_confusions.rb +379 -0
  54. data/lib/familia/base.rb +49 -10
  55. data/lib/familia/connection/handlers.rb +223 -0
  56. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  57. data/lib/familia/connection/middleware.rb +75 -0
  58. data/lib/familia/connection/operation_core.rb +93 -0
  59. data/lib/familia/connection/operations.rb +277 -0
  60. data/lib/familia/connection/pipeline_core.rb +87 -0
  61. data/lib/familia/connection/transaction_core.rb +100 -0
  62. data/lib/familia/connection.rb +60 -186
  63. data/lib/familia/data_type/commands.rb +53 -51
  64. data/lib/familia/data_type/serialization.rb +108 -107
  65. data/lib/familia/data_type/types/counter.rb +1 -1
  66. data/lib/familia/data_type/types/hashkey.rb +13 -10
  67. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  68. data/lib/familia/data_type/types/lock.rb +3 -2
  69. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  70. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  71. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  72. data/lib/familia/data_type.rb +75 -47
  73. data/lib/familia/distinguisher.rb +85 -0
  74. data/lib/familia/encryption/encrypted_data.rb +15 -24
  75. data/lib/familia/encryption/manager.rb +6 -4
  76. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  77. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  78. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  79. data/lib/familia/encryption/request_cache.rb +7 -7
  80. data/lib/familia/encryption.rb +2 -3
  81. data/lib/familia/errors.rb +9 -3
  82. data/lib/familia/features/autoloader.rb +30 -12
  83. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  84. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  85. data/lib/familia/features/encrypted_fields.rb +66 -64
  86. data/lib/familia/features/expiration/extensions.rb +1 -1
  87. data/lib/familia/features/expiration.rb +31 -26
  88. data/lib/familia/features/external_identifier.rb +9 -12
  89. data/lib/familia/features/object_identifier.rb +56 -19
  90. data/lib/familia/features/quantization.rb +16 -21
  91. data/lib/familia/features/relationships/README.md +97 -0
  92. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  93. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  94. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  95. data/lib/familia/features/relationships/indexing.rb +176 -256
  96. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  97. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  98. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  99. data/lib/familia/features/relationships/participation.rb +656 -0
  100. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  101. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  102. data/lib/familia/features/relationships.rb +65 -266
  103. data/lib/familia/features/safe_dump.rb +127 -130
  104. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  105. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  106. data/lib/familia/features/transient_fields.rb +3 -5
  107. data/lib/familia/features.rb +4 -13
  108. data/lib/familia/field_type.rb +24 -4
  109. data/lib/familia/horreum/core/connection.rb +229 -26
  110. data/lib/familia/horreum/core/database_commands.rb +27 -17
  111. data/lib/familia/horreum/core/serialization.rb +40 -20
  112. data/lib/familia/horreum/core/utils.rb +2 -1
  113. data/lib/familia/horreum/shared/settings.rb +2 -1
  114. data/lib/familia/horreum/subclass/definition.rb +33 -45
  115. data/lib/familia/horreum/subclass/management.rb +72 -24
  116. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  117. data/lib/familia/horreum.rb +196 -114
  118. data/lib/familia/json_serializer.rb +0 -1
  119. data/lib/familia/logging.rb +11 -114
  120. data/lib/familia/refinements/dear_json.rb +122 -0
  121. data/lib/familia/refinements/logger_trace.rb +20 -17
  122. data/lib/familia/refinements/stylize_words.rb +65 -0
  123. data/lib/familia/refinements/time_literals.rb +60 -52
  124. data/lib/familia/refinements.rb +2 -1
  125. data/lib/familia/secure_identifier.rb +60 -28
  126. data/lib/familia/settings.rb +83 -7
  127. data/lib/familia/utils.rb +5 -87
  128. data/lib/familia/verifiable_identifier.rb +4 -4
  129. data/lib/familia/version.rb +1 -1
  130. data/lib/familia.rb +72 -14
  131. data/lib/middleware/database_middleware.rb +56 -14
  132. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  133. data/try/configuration/scenarios_try.rb +1 -1
  134. data/try/connection/fiber_context_preservation_try.rb +250 -0
  135. data/try/connection/handler_constraints_try.rb +59 -0
  136. data/try/connection/operation_mode_guards_try.rb +208 -0
  137. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  138. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  139. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  140. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  141. data/try/connection/transaction_mode_strict_try.rb +98 -0
  142. data/try/connection/transaction_mode_warn_try.rb +131 -0
  143. data/try/connection/transaction_modes_try.rb +249 -0
  144. data/try/core/autoloader_try.rb +120 -2
  145. data/try/core/connection_try.rb +7 -7
  146. data/try/core/conventional_inheritance_try.rb +130 -0
  147. data/try/core/create_method_try.rb +15 -23
  148. data/try/core/database_consistency_try.rb +10 -10
  149. data/try/core/errors_try.rb +8 -11
  150. data/try/core/familia_extended_try.rb +2 -2
  151. data/try/core/familia_members_methods_try.rb +76 -0
  152. data/try/core/isolated_dbclient_try.rb +165 -0
  153. data/try/core/middleware_try.rb +16 -16
  154. data/try/core/persistence_operations_try.rb +4 -4
  155. data/try/core/pools_try.rb +42 -26
  156. data/try/core/secure_identifier_try.rb +28 -24
  157. data/try/core/time_utils_try.rb +10 -10
  158. data/try/core/tools_try.rb +1 -1
  159. data/try/core/utils_try.rb +2 -2
  160. data/try/data_types/boolean_try.rb +4 -4
  161. data/try/data_types/datatype_base_try.rb +0 -2
  162. data/try/data_types/list_try.rb +10 -10
  163. data/try/data_types/sorted_set_try.rb +5 -5
  164. data/try/data_types/string_try.rb +12 -12
  165. data/try/data_types/unsortedset_try.rb +33 -0
  166. data/try/debugging/cache_behavior_tracer.rb +7 -7
  167. data/try/debugging/debug_aad_process.rb +1 -1
  168. data/try/debugging/debug_concealed_internal.rb +1 -1
  169. data/try/debugging/debug_cross_context.rb +1 -1
  170. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  171. data/try/debugging/encryption_method_tracer.rb +10 -10
  172. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  173. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  174. data/try/encryption/config_persistence_try.rb +2 -2
  175. data/try/encryption/encryption_core_try.rb +19 -19
  176. data/try/encryption/instance_variable_scope_try.rb +1 -1
  177. data/try/encryption/module_loading_try.rb +2 -2
  178. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  179. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  180. data/try/encryption/secure_memory_handling_try.rb +1 -1
  181. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  182. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  183. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  184. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  185. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  186. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  187. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  188. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  189. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  190. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  191. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  192. data/try/features/feature_dependencies_try.rb +3 -3
  193. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  194. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  195. data/try/features/quantization/quantization_try.rb +1 -1
  196. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  197. data/try/features/relationships/indexing_try.rb +433 -0
  198. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  199. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  200. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  201. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  202. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  203. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  204. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  205. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  206. data/try/features/relationships/relationships_performance_try.rb +20 -20
  207. data/try/features/relationships/relationships_try.rb +27 -38
  208. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  209. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  210. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  211. data/try/helpers/test_cleanup.rb +86 -0
  212. data/try/helpers/test_helpers.rb +3 -3
  213. data/try/horreum/base_try.rb +3 -2
  214. data/try/horreum/commands_try.rb +1 -1
  215. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  216. data/try/horreum/initialization_try.rb +11 -7
  217. data/try/horreum/relations_try.rb +21 -13
  218. data/try/horreum/serialization_try.rb +12 -11
  219. data/try/integration/cross_component_try.rb +3 -3
  220. data/try/memory/memory_basic_test.rb +1 -1
  221. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  222. data/try/models/customer_safe_dump_try.rb +1 -1
  223. data/try/models/customer_try.rb +8 -10
  224. data/try/models/datatype_base_try.rb +3 -3
  225. data/try/models/familia_object_try.rb +9 -8
  226. data/try/performance/benchmarks_try.rb +2 -2
  227. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  228. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  229. data/try/prototypes/atomic_saves_v4.rb +1 -1
  230. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  231. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  232. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  234. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  235. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  236. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  237. data/try/prototypes/pooling/pool_siege.rb +11 -11
  238. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  239. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  240. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  241. data/try/refinements/logger_trace_methods_try.rb +44 -0
  242. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  243. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  244. metadata +75 -43
  245. data/.rubocop_todo.yml +0 -208
  246. data/docs/connection_pooling.md +0 -192
  247. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  248. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  249. data/docs/guides/Feature-System-Autoloading.md +0 -198
  250. data/docs/guides/Home.md +0 -116
  251. data/docs/guides/Relationships-Guide.md +0 -737
  252. data/docs/guides/relationships-methods.md +0 -266
  253. data/docs/reference/auditing_database_commands.rb +0 -228
  254. data/examples/permissions.rb +0 -240
  255. data/lib/familia/features/relationships/cascading.rb +0 -437
  256. data/lib/familia/features/relationships/membership.rb +0 -497
  257. data/lib/familia/features/relationships/permission_management.rb +0 -264
  258. data/lib/familia/features/relationships/querying.rb +0 -615
  259. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  260. data/lib/familia/features/relationships/tracking.rb +0 -418
  261. data/lib/familia/refinements/snake_case.rb +0 -40
  262. data/lib/familia/validation/command_recorder.rb +0 -336
  263. data/lib/familia/validation/expectations.rb +0 -519
  264. data/lib/familia/validation/validation_helpers.rb +0 -443
  265. data/lib/familia/validation/validator.rb +0 -412
  266. data/lib/familia/validation.rb +0 -140
  267. data/try/data_types/set_try.rb +0 -33
  268. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  269. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  270. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  271. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  272. data/try/validation/command_validation_try.rb.disabled +0 -207
  273. data/try/validation/performance_validation_try.rb.disabled +0 -324
  274. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -32,22 +32,22 @@ module Familia
32
32
  @dump_method = nil
33
33
  @load_method = nil
34
34
 
35
- # DefinitionMethods: Provides class-level functionality for Horreum subclasses
35
+ # DefinitionMethods - Class-level DSL methods for defining Horreum model structure
36
36
  #
37
37
  # This module is extended into classes that include Familia::Horreum,
38
- # providing methods for Database operations and object management.
38
+ # providing class methods for defining model structure and configuration
39
+ # (e.g., Customer.field :name, Customer.identifier_field :custid).
39
40
  #
40
41
  # Key features:
41
- # * Includes RelatedFieldsManagement for DataType field handling
42
- # * Defines methods for managing fields, identifiers, and dbkeys
43
- # * Provides utility methods for working with Database objects
42
+ # * Defines DSL methods for field definitions (field, identifier_field)
43
+ # * Includes RelatedFieldsManagement for DataType field DSL (list, set, zset, etc.)
44
+ # * Provides class-level configuration (prefix, suffix, logical_database)
45
+ # * Manages field metadata and inheritance
44
46
  #
45
47
  module DefinitionMethods
46
48
  include Familia::Settings
47
49
  include Familia::Horreum::RelatedFieldsManagement # Provides DataType field methods
48
50
 
49
- using Familia::Refinements::SnakeCase
50
-
51
51
  # Sets or retrieves the unique identifier field for the class.
52
52
  #
53
53
  # This method defines or returns the field or method that contains the unique
@@ -77,13 +77,13 @@ module Familia
77
77
  #
78
78
  # This method defines a new field for the class, creating getter and setter
79
79
  # instance methods similar to `attr_accessor`. It also generates a fast
80
- # writer method for immediate persistence to Redis.
80
+ # writer method for immediate persistence to the database.
81
81
  #
82
82
  # @param name [Symbol, String] the name of the field to define. If a method
83
83
  # with the same name already exists, an error is raised.
84
84
  # @param as [Symbol, String, false, nil] as the name to use for the accessor method (defaults to name).
85
85
  # If false or nil, no accessor methods are created.
86
- # @param fast_method [Symbol, false, nil] the name to use for the fast writer method (defaults to :"#{name}!").
86
+ # @param fast_method [Symbol, false, nil] the name to use for the fast writer method (defaults to :`"#{name}!"`).
87
87
  # If false or nil, no fast writer method is created.
88
88
  # @param on_conflict [Symbol] conflict resolution strategy when method already exists:
89
89
  # - :raise - raise error if method exists (default)
@@ -117,7 +117,7 @@ module Familia
117
117
  register_field_type(field_type)
118
118
  end
119
119
 
120
- # Sets or retrieves the suffix for generating Redis keys.
120
+ # Sets or retrieves the suffix for generating Valkey/Redis keys.
121
121
  #
122
122
  # @param a [String, Symbol, nil] the suffix to set (optional).
123
123
  # @param blk [Proc] a block that returns the suffix (optional).
@@ -128,7 +128,7 @@ module Familia
128
128
  @suffix || Familia.default_suffix
129
129
  end
130
130
 
131
- # Sets or retrieves the prefix for generating Redis keys.
131
+ # Sets or retrieves the prefix for generating Valkey/Redis keys.
132
132
  #
133
133
  # @param a [String, Symbol, nil] the prefix to set (optional).
134
134
  # @return [String, Symbol] the current prefix.
@@ -144,12 +144,12 @@ module Familia
144
144
  raise Problem, 'Cannot generate prefix for anonymous class. ' \
145
145
  'Use `prefix` method to set explicitly.'
146
146
  end
147
- name.downcase.gsub('::', Familia.delim).to_sym
147
+ config_name.to_sym
148
148
  end
149
149
  end
150
150
 
151
151
  def logical_database(v = nil)
152
- Familia.trace :DB, Familia.dbclient, "#{@logical_database} #{v.nil?}", caller(0..2) if Familia.debug?
152
+ Familia.trace :LOGICAL_DATABASE_DEF, "instvar:#{@logical_database}", v if Familia.debug?
153
153
  @logical_database = v unless v.nil?
154
154
  @logical_database || parent&.logical_database
155
155
  end
@@ -172,20 +172,7 @@ module Familia
172
172
  end
173
173
 
174
174
  def relations?
175
- @has_relations ||= false
176
- end
177
-
178
- # Converts the class name into a string that can be used to look up
179
- # configuration values. This is particularly useful when mapping
180
- # familia models with specific database numbers in the configuration.
181
- #
182
- # Familia::Horreum::DefinitionMethods#config_name
183
- #
184
- # @example V2::Session.config_name => 'session'
185
- #
186
- # @return [String] The underscored class name as a string
187
- def config_name
188
- name.snake_case
175
+ @has_related_fields ||= false
189
176
  end
190
177
 
191
178
  def dump_method
@@ -234,6 +221,8 @@ module Familia
234
221
  # Complete the registration after installation. If we do this beforehand
235
222
  # we can run into issues where it looks like it's already installed.
236
223
  field_types[field_type.name] = field_type
224
+ # Freeze the field_type to ensure immutability (maintains Data class heritage)
225
+ field_type.freeze
237
226
  end
238
227
 
239
228
  # Retrieves feature options for the current class.
@@ -303,21 +292,20 @@ module Familia
303
292
  # end
304
293
  #
305
294
  def add_feature_options(feature_name, **options)
306
- @feature_options ||= {}
307
- @feature_options[feature_name.to_sym] ||= {}
295
+ @feature_options ||= {}
296
+ @feature_options[feature_name.to_sym] ||= {}
308
297
 
309
- # Only set defaults for options that don't already exist
310
- options.each do |key, value|
311
- @feature_options[feature_name.to_sym][key] ||= value
312
- end
298
+ # Only set defaults for options that don't already exist
299
+ options.each do |key, value|
300
+ @feature_options[feature_name.to_sym][key] ||= value
301
+ end
313
302
 
314
- @feature_options[feature_name.to_sym]
315
- end
303
+ @feature_options[feature_name.to_sym]
304
+ end
316
305
 
317
306
  # Create and register a transient field type
318
307
  #
319
308
  # @param name [Symbol] The field name
320
- # @param options [Hash] Field options
321
309
  #
322
310
  def transient_field(name, **)
323
311
  require_relative '../../features/transient_fields/transient_field_type'
@@ -379,8 +367,8 @@ end
379
367
  # @raise [ArgumentError] if fast_method_name doesn't end with '!'
380
368
  #
381
369
  # @note Generated method behavior:
382
- # - Without args: Retrieves current value from Redis
383
- # - With value: Sets and immediately persists to Redis
370
+ # - Without args: Retrieves current value from Valkey/Redis
371
+ # - With value: Sets and immediately persists to Valkey/Redis
384
372
  # - Returns boolean indicating success for writes
385
373
  # - Bypasses object-level caching and expiration updates
386
374
  #
@@ -394,20 +382,20 @@ end
394
382
  handle_method_conflict(fast_method_name, on_conflict) do
395
383
  # Fast attribute accessor method for the '#{field_name}' attribute.
396
384
  # This method provides immediate read and write access to the attribute
397
- # in Redis.
385
+ # in the database.
398
386
  #
399
387
  # When called without arguments, it retrieves the current value of the
400
- # attribute from Redis.
388
+ # attribute from the database.
401
389
  # When called with an argument, it immediately persists the new value to
402
- # Redis.
390
+ # the database.
403
391
  #
404
392
  # @overload #{method_name}
405
- # Retrieves the current value of the attribute from Redis.
393
+ # Retrieves the current value of the attribute from the database.
406
394
  # @return [Object] the current value of the attribute.
407
395
  #
408
396
  # @overload #{method_name}(value)
409
397
  # Sets and immediately persists the new value of the attribute to
410
- # Redis.
398
+ # the database.
411
399
  # @param value [Object] the new value to set for the attribute.
412
400
  # @return [Object] the newly set value.
413
401
  #
@@ -416,7 +404,7 @@ end
416
404
  # the method.
417
405
  #
418
406
  # @note This method bypasses any object-level caching and interacts
419
- # directly with Redis. It does not trigger updates to other attributes
407
+ # directly with the database. It does not trigger updates to other attributes
420
408
  # or the object's expiration time.
421
409
  #
422
410
  # @example
@@ -437,7 +425,7 @@ end
437
425
 
438
426
  begin
439
427
  # Trace the operation if debugging is enabled.
440
- Familia.trace :FAST_WRITER, dbclient, "#{field_name}: #{val.inspect}", caller(1..1) if Familia.debug?
428
+ Familia.trace :FAST_WRITER, nil, "#{field_name}: #{val.inspect}" if Familia.debug?
441
429
 
442
430
  # Convert the provided value to a format suitable for Database storage.
443
431
  prepared = serialize_value(val)
@@ -4,11 +4,11 @@ require_relative 'related_fields_management'
4
4
 
5
5
  module Familia
6
6
  class Horreum
7
- # ManagementMethods: Provides class-level functionality for Horreum
8
- # records.
7
+ # ManagementMethods - Class-level methods for Horreum model management
9
8
  #
10
9
  # This module is extended into classes that include Familia::Horreum,
11
- # providing methods for Database operations and object management.
10
+ # providing class methods for database operations and object management
11
+ # (e.g., Customer.create, Customer.find_by_id)
12
12
  #
13
13
  # # Key features:
14
14
  # * Includes RelatedFieldsManagement for DataType field handling
@@ -17,11 +17,13 @@ module Familia
17
17
  module ManagementMethods
18
18
  include Familia::Horreum::RelatedFieldsManagement # Provides DataType query methods
19
19
 
20
+ using Familia::Refinements::StylizeWords
21
+
20
22
  # Creates and persists a new instance of the class.
21
23
  #
22
- # @param *args [Array] Variable number of positional arguments to be passed
24
+ # @param args [Array] Variable number of positional arguments to be passed
23
25
  # to the constructor.
24
- # @param **kwargs [Hash] Keyword arguments to be passed to the constructor.
26
+ # @param kwargs [Hash] Keyword arguments to be passed to the constructor.
25
27
  # @return [Object] The newly created and persisted instance.
26
28
  # @raise [Familia::Problem] If an instance with the same identifier already
27
29
  # exists.
@@ -68,10 +70,35 @@ module Familia
68
70
  ids.collect! { |objid| dbkey(objid) }
69
71
  return [] if ids.compact.empty?
70
72
 
71
- Familia.trace :MULTIGET, dbclient, "#{ids.size}: #{ids}", caller(1..1) if Familia.debug?
73
+ Familia.trace :MULTIGET, nil, "#{ids.size}: #{ids}" if Familia.debug?
72
74
  dbclient.mget(*ids)
73
75
  end
74
76
 
77
+ # Converts the class name into a string that can be used to look up
78
+ # configuration values. This is particularly useful when mapping
79
+ # familia models with specific database numbers in the configuration.
80
+ #
81
+ # Familia::Horreum::DefinitionMethods#config_name
82
+ #
83
+ # @example V2::Session.config_name => 'session'
84
+ #
85
+ # @return [String] The underscored class name as a string
86
+ def config_name
87
+ return nil if name.nil?
88
+
89
+ name.demodularize.snake_case
90
+ end
91
+
92
+ # Familia::Horreum::DefinitionMethods#familia_name
93
+ #
94
+ # @example V2::Session.config_name => 'Session'
95
+ #
96
+ def familia_name
97
+ return nil if name.nil?
98
+
99
+ name.demodularize
100
+ end
101
+
75
102
  # Retrieves and instantiates an object from Database using the full object
76
103
  # key.
77
104
  #
@@ -83,7 +110,7 @@ module Familia
83
110
  # This method performs a two-step process to safely retrieve and
84
111
  # instantiate objects:
85
112
  #
86
- # 1. It first checks if the key exists in Redis. This is crucial because:
113
+ # 1. It first checks if the key exists in the database. This is crucial because:
87
114
  # - It provides a definitive answer about the object's existence.
88
115
  # - It prevents ambiguity that could arise from `hgetall` returning an
89
116
  # empty hash for non-existent keys, which could lead to the creation
@@ -93,7 +120,7 @@ module Familia
93
120
  # it.
94
121
  #
95
122
  # This approach ensures that we only attempt to instantiate objects that
96
- # actually exist in Redis, improving reliability and simplifying
123
+ # actually exist in Valkey/Redis, improving reliability and simplifying
97
124
  # debugging.
98
125
  #
99
126
  # @example
@@ -108,17 +135,17 @@ module Familia
108
135
  does_exist = dbclient.exists(objkey).positive?
109
136
 
110
137
  Familia.ld "[.find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
111
- Familia.trace :FROM_KEY, dbclient, objkey, caller(1..1) if Familia.debug?
138
+ Familia.trace :FROM_KEY, nil, objkey if Familia.debug?
112
139
 
113
140
  # This is the reason for calling exists first. We want to definitively
114
- # and without any ambiguity know if the object exists in Redis. If it
141
+ # and without any ambiguity know if the object exists in the database. If it
115
142
  # doesn't, we return nil. If it does, we proceed to load the object.
116
143
  # Otherwise, hgetall will return an empty hash, which will be passed to
117
144
  # the constructor, which will then be annoying to debug.
118
145
  return unless does_exist
119
146
 
120
147
  obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
121
- Familia.trace :FROM_KEY2, dbclient, "#{objkey}: #{obj.inspect}", caller(1..1) if Familia.debug?
148
+ Familia.trace :FROM_KEY2, nil, "#{objkey}: #{obj.inspect}" if Familia.debug?
122
149
 
123
150
  new(**obj)
124
151
  end
@@ -150,33 +177,34 @@ module Familia
150
177
  objkey = dbkey(identifier, suffix)
151
178
 
152
179
  Familia.ld "[.find_by_id] #{self} from key #{objkey})"
153
- Familia.trace :FIND_BY_ID, Familia.dbclient(uri), objkey, caller(1..1).first if Familia.debug?
180
+ Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
154
181
  find_by_key objkey
155
182
  end
156
183
  alias find find_by_id
157
184
  alias load find_by_id # deprecated
158
185
  alias from_identifier find_by_id # deprecated
159
186
 
160
- # Checks if an object with the given identifier exists in Redis.
187
+ # Checks if an object with the given identifier exists in the database.
161
188
  #
162
189
  # @param identifier [String, Integer] The unique identifier for the object.
163
190
  # @param suffix [Symbol, nil] The suffix to use in the dbkey (default: class suffix).
164
191
  # @return [Boolean] true if the object exists, false otherwise.
165
192
  #
166
193
  # This method constructs the full dbkey using the provided identifier and suffix,
167
- # then checks if the key exists in Redis.
194
+ # then checks if the key exists in the database.
168
195
  #
169
196
  # @example
170
- # User.exists?(123) # Returns true if user:123:object exists in Redis
197
+ # User.exists?(123) # Returns true if user:123:object exists in Valkey/Redis
171
198
  #
172
199
  def exists?(identifier, suffix = nil)
173
- raise NoIdentifier, "Empty identifier" if identifier.to_s.empty?
200
+ raise NoIdentifier, 'Empty identifier' if identifier.to_s.empty?
201
+
174
202
  suffix ||= self.suffix
175
203
 
176
204
  objkey = dbkey identifier, suffix
177
205
 
178
206
  ret = dbclient.exists objkey
179
- Familia.trace :EXISTS, dbclient, "#{objkey} #{ret.inspect}", caller(1..1) if Familia.debug?
207
+ Familia.trace :EXISTS, nil, "#{objkey} #{ret.inspect}" if Familia.debug?
180
208
 
181
209
  ret.positive? # differs from Valkey API but I think it's okay bc `exists?` is a predicate method.
182
210
  end
@@ -193,17 +221,36 @@ module Familia
193
221
  # `delete!` when working directly with dbkeys.
194
222
  #
195
223
  # @example
196
- # User.destroy!(123) # Removes user:123:object from Redis
224
+ # User.destroy!(123) # Removes user:123:object from Valkey/Redis
197
225
  #
198
226
  def destroy!(identifier, suffix = nil)
199
227
  suffix ||= self.suffix
200
- return false if identifier.to_s.empty?
228
+ return MultiResult.new(false, []) if identifier.to_s.empty?
201
229
 
202
230
  objkey = dbkey identifier, suffix
203
231
 
204
- ret = dbclient.del objkey
205
- Familia.trace :DESTROY!, dbclient, "#{objkey} #{ret.inspect}", caller(1..1) if Familia.debug?
206
- ret.positive?
232
+ # Execute all deletion operations within a transaction
233
+ transaction do |conn|
234
+ # Clean up related fields first to avoid orphaned keys
235
+ if relations?
236
+ Familia.trace :DESTROY_RELATIONS!, nil, "#{self} has relations: #{related_fields.keys}" if Familia.debug?
237
+
238
+ # Create a temporary instance to access related fields.
239
+ # Pass identifier in constructor so init() sees it and can set dependent fields.
240
+ identifier_field_name = self.identifier_field
241
+ temp_instance = identifier_field_name ? new(identifier_field_name => identifier.to_s) : new
242
+
243
+ related_fields.each do |name, _definition|
244
+ obj = temp_instance.send(name)
245
+ Familia.trace :DESTROY_RELATION!, name, "Deleting related field #{name} (#{obj.dbkey})" if Familia.debug?
246
+ conn.del(obj.dbkey)
247
+ end
248
+ end
249
+
250
+ # Delete the main object key
251
+ ret = conn.del(objkey)
252
+ Familia.trace :DESTROY!, nil, "#{objkey} #{ret.inspect}" if Familia.debug?
253
+ end
207
254
  end
208
255
 
209
256
  # Finds all keys in Database matching the given suffix pattern.
@@ -249,7 +296,7 @@ module Familia
249
296
  end
250
297
 
251
298
  def any?(filter = '*')
252
- matching_keys_count(filter) > 0
299
+ matching_keys_count(filter).positive?
253
300
  end
254
301
 
255
302
  # Returns the number of dbkeys matching the given filter pattern
@@ -259,7 +306,8 @@ module Familia
259
306
  def matching_keys_count(filter = '*')
260
307
  dbclient.keys(dbkey(filter)).compact.size
261
308
  end
262
- alias size matching_keys_count # For backwards compatibility
309
+ alias size matching_keys_count
310
+ alias length matching_keys_count
263
311
  end
264
312
  end
265
313
  end
@@ -1,32 +1,99 @@
1
1
  # lib/familia/horreum/related_fields_management.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,16 +162,13 @@ 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
173
  attr_reader name
110
174
 
@@ -120,17 +184,14 @@ module Familia
120
184
 
121
185
  # Creates a class-level relation
122
186
  def attach_class_related_field(name, klass, opts)
123
- Familia.trace :attach_class_related_field, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
187
+ Familia.trace :attach_class_related_field, "#{name} #{klass}", opts if Familia.debug?
124
188
  raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty?
125
189
 
126
190
  name = name.to_s.to_sym
127
191
  opts = opts.nil? ? {} : opts.clone
128
192
  opts[:parent] = self unless opts.key?(:parent)
129
193
 
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
194
+ class_related_fields[name] = RelatedFieldDefinition.new(name, klass, opts)
134
195
 
135
196
  # An accessor method created in the metaclass will
136
197
  # access the instance variables for this class.