familia 2.0.0.pre16 → 2.0.0.pre18

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 (250) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/{code-smellage.yml → code-smells.yml} +3 -63
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +6 -0
  6. data/CHANGELOG.rst +82 -0
  7. data/CLAUDE.md +47 -2
  8. data/Gemfile.lock +1 -1
  9. data/README.md +13 -0
  10. data/bin/irb +1 -1
  11. data/docs/archive/FAMILIA_TECHNICAL.md +1 -1
  12. data/docs/guides/core-field-system.md +48 -26
  13. data/docs/migrating/v2.0.0-pre18.md +58 -0
  14. data/docs/overview.md +2 -2
  15. data/docs/qodo-merge-compliance.md +96 -0
  16. data/docs/reference/api-technical.md +1 -1
  17. data/examples/encrypted_fields.rb +1 -1
  18. data/examples/safe_dump.rb +1 -1
  19. data/lib/familia/base.rb +6 -6
  20. data/lib/familia/connection/middleware.rb +58 -4
  21. data/lib/familia/connection.rb +1 -1
  22. data/lib/familia/data_type/class_methods.rb +63 -0
  23. data/lib/familia/data_type/connection.rb +83 -0
  24. data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
  25. data/lib/familia/data_type/serialization.rb +5 -5
  26. data/lib/familia/data_type/settings.rb +96 -0
  27. data/lib/familia/data_type/types/hashkey.rb +2 -1
  28. data/lib/familia/data_type/types/sorted_set.rb +113 -10
  29. data/lib/familia/data_type/types/stringkey.rb +0 -4
  30. data/lib/familia/data_type.rb +8 -195
  31. data/lib/familia/encryption/encrypted_data.rb +12 -2
  32. data/lib/familia/encryption/manager.rb +11 -4
  33. data/lib/familia/features/autoloader.rb +3 -1
  34. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  35. data/lib/familia/features/encrypted_fields.rb +5 -2
  36. data/lib/familia/features/external_identifier.rb +49 -8
  37. data/lib/familia/features/object_identifier.rb +84 -12
  38. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
  39. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +45 -26
  40. data/lib/familia/features/relationships/indexing.rb +7 -1
  41. data/lib/familia/features/relationships/participation/participant_methods.rb +6 -2
  42. data/lib/familia/features/safe_dump.rb +2 -3
  43. data/lib/familia/features/transient_fields.rb +7 -2
  44. data/lib/familia/features.rb +6 -1
  45. data/lib/familia/field_type.rb +0 -18
  46. data/lib/familia/horreum/{core/connection.rb → connection.rb} +21 -0
  47. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +1 -1
  48. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +102 -56
  49. data/lib/familia/horreum/{subclass/management.rb → management.rb} +18 -15
  50. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +73 -170
  51. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +22 -2
  52. data/lib/familia/horreum/serialization.rb +190 -0
  53. data/lib/familia/horreum.rb +39 -14
  54. data/lib/familia/identifier_extractor.rb +60 -0
  55. data/lib/familia/logging.rb +271 -112
  56. data/lib/familia/refinements.rb +0 -1
  57. data/lib/familia/version.rb +1 -1
  58. data/lib/familia.rb +2 -2
  59. data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
  60. data/pr_agent.toml +31 -0
  61. data/pr_compliance_checklist.yaml +45 -0
  62. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  63. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  64. data/try/edge_cases/json_serialization_try.rb +2 -2
  65. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  66. data/try/edge_cases/race_conditions_try.rb +1 -1
  67. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  68. data/try/edge_cases/string_coercion_try.rb +1 -1
  69. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  70. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  71. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  72. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  73. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  74. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  75. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  76. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  77. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  78. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  79. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  80. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  81. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  82. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  83. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  84. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  85. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  86. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  87. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  88. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  89. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  90. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  91. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  92. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  93. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  94. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  95. data/try/features/expiration/expiration_try.rb +1 -1
  96. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  97. data/try/features/feature_dependencies_try.rb +1 -1
  98. data/try/features/feature_improvements_try.rb +1 -1
  99. data/try/features/field_groups_try.rb +244 -0
  100. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  101. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  102. data/try/features/quantization/quantization_try.rb +1 -1
  103. data/try/features/real_feature_integration_try.rb +17 -14
  104. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  105. data/try/features/relationships/indexing_try.rb +16 -1
  106. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  107. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  108. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  109. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  110. data/try/features/relationships/relationships_api_changes_try.rb +1 -1
  111. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  112. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  113. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  114. data/try/features/relationships/relationships_performance_try.rb +1 -1
  115. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  116. data/try/features/relationships/relationships_try.rb +1 -1
  117. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  118. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  119. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  120. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  121. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  122. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  123. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  124. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
  125. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  126. data/try/{core → integration/connection}/isolated_dbclient_try.rb +3 -3
  127. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  128. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
  129. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
  130. data/try/{core → integration/connection}/pools_try.rb +1 -1
  131. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  132. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  133. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  134. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  135. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  136. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  137. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  138. data/try/{core → integration}/create_method_try.rb +1 -1
  139. data/try/integration/cross_component_try.rb +1 -1
  140. data/try/{core → integration}/database_consistency_try.rb +12 -8
  141. data/try/{core → integration}/familia_extended_try.rb +1 -1
  142. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  143. data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
  144. data/try/{models → integration/models}/customer_try.rb +6 -6
  145. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  146. data/try/{models → integration/models}/familia_object_try.rb +1 -1
  147. data/try/{core → integration}/persistence_operations_try.rb +1 -1
  148. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  149. data/try/{configuration → integration}/scenarios_try.rb +2 -2
  150. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  151. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  152. data/try/performance/benchmarks_try.rb +2 -2
  153. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  154. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  155. data/try/{helpers → support/helpers}/test_helpers.rb +15 -7
  156. data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +1 -1
  157. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  158. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  159. data/try/{core → unit/core}/connection_try.rb +5 -5
  160. data/try/{core → unit/core}/errors_try.rb +4 -4
  161. data/try/{core → unit/core}/extensions_try.rb +1 -1
  162. data/try/unit/core/familia_logger_try.rb +110 -0
  163. data/try/{core → unit/core}/familia_try.rb +2 -2
  164. data/try/{core → unit/core}/middleware_try.rb +41 -1
  165. data/try/{core → unit/core}/settings_try.rb +1 -1
  166. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  167. data/try/{core → unit/core}/tools_try.rb +3 -3
  168. data/try/{core → unit/core}/utils_try.rb +17 -14
  169. data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
  170. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  171. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  172. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  173. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  174. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  175. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  176. data/try/unit/data_types/sorted_set_zadd_options_try.rb +625 -0
  177. data/try/{data_types → unit/data_types}/string_try.rb +1 -1
  178. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  179. data/try/unit/horreum/auto_indexing_on_save_try.rb +212 -0
  180. data/try/{horreum → unit/horreum}/base_try.rb +3 -3
  181. data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
  182. data/try/{horreum → unit/horreum}/commands_try.rb +3 -1
  183. data/try/unit/horreum/defensive_initialization_try.rb +86 -0
  184. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +3 -1
  185. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  186. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  187. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  188. data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
  189. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  190. data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
  191. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  192. data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
  193. data/try/{horreum → unit/horreum}/settings_try.rb +3 -1
  194. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  195. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  196. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  197. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  198. data/try/valkey.conf +26 -0
  199. metadata +149 -132
  200. data/lib/familia/distinguisher.rb +0 -85
  201. data/lib/familia/horreum/core.rb +0 -21
  202. data/lib/familia/refinements/logger_trace.rb +0 -60
  203. data/try/refinements/logger_trace_methods_try.rb +0 -44
  204. /data/lib/familia/horreum/{shared/settings.rb → settings.rb} +0 -0
  205. /data/lib/familia/horreum/{core/utils.rb → utils.rb} +0 -0
  206. /data/try/{debugging → support/debugging}/README.md +0 -0
  207. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  208. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  209. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  210. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  211. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  212. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  213. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  214. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  215. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  216. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  217. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  218. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  219. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  220. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  221. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  222. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  223. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  224. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  225. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  226. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  227. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  228. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  229. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  230. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  231. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  232. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  233. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  234. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  235. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  236. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  237. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  238. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  239. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  240. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  241. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  242. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  243. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  244. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  245. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  246. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  247. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  248. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  249. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  250. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -33,9 +33,12 @@ module Familia
33
33
  # - Employee.rebuild_email_index
34
34
  #
35
35
  # Generates on Employee (self):
36
- # - employee.add_to_class_email_index
36
+ # - employee.add_to_class_email_index (called automatically on save)
37
37
  # - employee.remove_from_class_email_index
38
38
  # - employee.update_in_class_email_index(old_email)
39
+ #
40
+ # Note: Class-level indexes auto-populate on save(). Instance-scoped indexes
41
+ # (with within:) remain manual as they require parent context.
39
42
  module UniqueIndexGenerators
40
43
  module_function
41
44
 
@@ -101,35 +104,42 @@ module Familia
101
104
 
102
105
  # Generate instance query method (e.g., company.find_by_badge_number)
103
106
  actual_target_class.class_eval do
104
- define_method("find_by_#{field}") do |field_value|
107
+ define_method(:"find_by_#{field}") do |provided_value|
105
108
  # Use declared field accessor instead of manual instantiation
106
109
  index_hash = send(index_name)
107
110
 
108
- # Get the identifier from the hash
109
- object_id = index_hash[field_value.to_s]
110
- return nil unless object_id
111
+ # Get the identifier from the hash using .get method.
112
+ # We use .get instead of [] because it's part of the standard interface
113
+ # common across all DataType classes (List, UnsortedSet, SortedSet, HashKey).
114
+ # While unique indexes always use HashKey, using .get maintains consistency
115
+ # with the broader DataType API patterns used throughout Familia.
116
+ record_id = index_hash.get(provided_value)
117
+ return nil unless record_id
111
118
 
112
- indexed_class.new(object_id)
119
+ indexed_class.find_by_identifier(record_id)
113
120
  end
114
121
 
115
122
  # Generate bulk query method (e.g., company.find_all_by_badge_number)
116
- define_method("find_all_by_#{field}") do |field_values|
117
- return [] if field_values.empty?
123
+ define_method(:"find_all_by_#{field}") do |provided_ids|
124
+ provided_ids = Array(provided_ids)
125
+ return [] if provided_ids.empty?
118
126
 
119
127
  # Use declared field accessor instead of manual instantiation
120
128
  index_hash = send(index_name)
121
129
 
122
130
  # Get all identifiers from the hash
123
- object_ids = index_hash.values_at(*field_values.map(&:to_s))
131
+ record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
124
132
  # Filter out nil values and instantiate objects
125
- object_ids.compact.map { |object_id| indexed_class.new(object_id) }
133
+ record_ids.compact.map { |record_id|
134
+ indexed_class.find_by_identifier(record_id)
135
+ }
126
136
  end
127
137
 
128
138
  # Accessor method already created by ensure_index_field above
129
139
  # No need to manually define it here
130
140
 
131
141
  # Generate method to rebuild the unique index for this parent instance
132
- define_method("rebuild_#{index_name}") do
142
+ define_method(:"rebuild_#{index_name}") do
133
143
  # Use declared field accessor instead of manual instantiation
134
144
  index_hash = send(index_name)
135
145
 
@@ -156,7 +166,7 @@ module Familia
156
166
  def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
157
167
  target_class_config = target_class.config_name
158
168
  indexed_class.class_eval do
159
- method_name = "add_to_#{target_class_config}_#{index_name}"
169
+ method_name = :"add_to_#{target_class_config}_#{index_name}"
160
170
  Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
161
171
 
162
172
  define_method(method_name) do |target_instance|
@@ -172,7 +182,7 @@ module Familia
172
182
  index_hash[field_value.to_s] = identifier
173
183
  end
174
184
 
175
- method_name = "remove_from_#{target_class_config}_#{index_name}"
185
+ method_name = :"remove_from_#{target_class_config}_#{index_name}"
176
186
  Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
177
187
 
178
188
  define_method(method_name) do |target_instance|
@@ -188,7 +198,7 @@ module Familia
188
198
  index_hash.remove(field_value.to_s)
189
199
  end
190
200
 
191
- method_name = "update_in_#{target_class_config}_#{index_name}"
201
+ method_name = :"update_in_#{target_class_config}_#{index_name}"
192
202
  Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
193
203
 
194
204
  define_method(method_name) do |target_instance, old_field_value = nil|
@@ -218,30 +228,39 @@ module Familia
218
228
  # - Employee.email_index
219
229
  # - Employee.rebuild_email_index
220
230
  def generate_query_methods_class(field, index_name, indexed_class)
221
- indexed_class.define_singleton_method("find_by_#{field}") do |field_value|
231
+ indexed_class.define_singleton_method(:"find_by_#{field}") do |provided_id|
222
232
  index_hash = send(index_name) # Access the class-level hashkey DataType
223
- object_id = index_hash[field_value.to_s]
224
233
 
225
- return nil unless object_id
234
+ # Get the identifier from the hash using .get method.
235
+ # We use .get instead of [] because it's part of the standard interface
236
+ # common across all DataType classes (List, UnsortedSet, SortedSet, HashKey).
237
+ # While unique indexes always use HashKey, using .get maintains consistency
238
+ # with the broader DataType API patterns used throughout Familia.
239
+ record_id = index_hash.get(provided_id)
240
+
241
+ return nil unless record_id
226
242
 
227
- new(object_id)
243
+ indexed_class.find_by_identifier(record_id)
228
244
  end
229
245
 
230
246
  # Generate class-level bulk query method
231
- indexed_class.define_singleton_method("find_all_by_#{field}") do |field_values|
232
- return [] if field_values.empty?
247
+ indexed_class.define_singleton_method(:"find_all_by_#{field}") do |provided_ids|
248
+ provided_ids = Array(provided_ids)
249
+ return [] if provided_ids.empty?
233
250
 
234
251
  index_hash = send(index_name) # Access the class-level hashkey DataType
235
- object_ids = index_hash.values_at(*field_values.map(&:to_s))
252
+ record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
236
253
  # Filter out nil values and instantiate objects
237
- object_ids.compact.map { |object_id| new(object_id) }
254
+ record_ids.compact.map { |record_id|
255
+ indexed_class.find_by_identifier(record_id)
256
+ }
238
257
  end
239
258
 
240
259
  # The index accessor method is already created by the class_hashkey declaration
241
260
  # No need to manually create it - Horreum handles this automatically
242
261
 
243
262
  # Generate method to rebuild the class-level index
244
- indexed_class.define_singleton_method("rebuild_#{index_name}") do
263
+ indexed_class.define_singleton_method(:"rebuild_#{index_name}") do
245
264
  index_hash = send(index_name) # Access the class-level hashkey DataType
246
265
 
247
266
  # Clear existing index using DataType method
@@ -260,7 +279,7 @@ module Familia
260
279
  # - employee.update_in_class_email_index(old_email)
261
280
  def generate_mutation_methods_class(field, index_name, indexed_class)
262
281
  indexed_class.class_eval do
263
- define_method("add_to_class_#{index_name}") do
282
+ define_method(:"add_to_class_#{index_name}") do
264
283
  index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
265
284
  field_value = send(field)
266
285
 
@@ -269,7 +288,7 @@ module Familia
269
288
  index_hash[field_value.to_s] = identifier
270
289
  end
271
290
 
272
- define_method("remove_from_class_#{index_name}") do
291
+ define_method(:"remove_from_class_#{index_name}") do
273
292
  index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
274
293
  field_value = send(field)
275
294
 
@@ -278,7 +297,7 @@ module Familia
278
297
  index_hash.remove(field_value.to_s)
279
298
  end
280
299
 
281
- define_method("update_in_class_#{index_name}") do |old_field_value = nil|
300
+ define_method(:"update_in_class_#{index_name}") do |old_field_value = nil|
282
301
  new_field_value = send(field)
283
302
 
284
303
  # Use class-level transaction for atomicity with DataType abstraction
@@ -18,7 +18,7 @@ module Familia
18
18
  # end
19
19
  #
20
20
  # user = User.new(user_id: 'u1', email: 'alice@example.com')
21
- # user.add_to_class_email_lookup
21
+ # user.save # Automatically populates email_lookup index
22
22
  # User.find_by_email('alice@example.com') # → user
23
23
  #
24
24
  # @example Instance-scoped unique index (within parent, 1:1 via HashKey)
@@ -58,6 +58,12 @@ module Familia
58
58
  # - Instance unique: "company:c1:badge_index" → HashKey
59
59
  # - Instance multi: "company:c1:dept_index:engineering" → UnsortedSet
60
60
  #
61
+ # Auto-Indexing:
62
+ # Class-level unique_index declarations automatically populate on save():
63
+ # user = User.new(email: 'test@example.com')
64
+ # user.save # Auto-indexes email → user_id
65
+ # Instance-scoped indexes (with within:) remain manual (require parent context).
66
+ #
61
67
  # Design Philosophy:
62
68
  # Indexing is for finding objects by attribute, not ordering them.
63
69
  # Use multi_index with UnsortedSet (no temporal scores), then sort in Ruby:
@@ -23,8 +23,10 @@ module Familia
23
23
  # ├── add_to_customer_domains(customer, score) # Add myself to customer's domains
24
24
  # ├── remove_from_customer_domains(customer) # Remove myself from customer's domains
25
25
  # ├── score_in_customer_domains(customer) # Get my score (sorted_set only)
26
- # ├── update_score_in_customer_domains(customer) # Update my score (sorted_set only)
27
26
  # └── position_in_customer_domains(customer) # Get my position (list only)
27
+ #
28
+ # Note: To update scores, use the DataType API directly:
29
+ # customer.domains.add(domain.identifier, new_score, xx: true)
28
30
 
29
31
  module Builder
30
32
  extend CollectionOperations
@@ -126,7 +128,9 @@ module Familia
126
128
 
127
129
  # Build score-related methods for sorted sets
128
130
  # Creates: domain.score_in_customer_domains(customer)
129
- # domain.update_score_in_customer_domains(customer, new_score)
131
+ #
132
+ # Note: Score updates use DataType API directly:
133
+ # customer.domains.add(domain.identifier, new_score, xx: true)
130
134
  def self.build_score_methods(participant_class, target_name, collection_name)
131
135
  # Get score method
132
136
  score_method = "score_in_#{target_name}_#{collection_name}"
@@ -1,9 +1,8 @@
1
1
  # lib/familia/features/safe_dump.rb
2
2
 
3
3
  #
4
- # Class instance variables are used here for feature configuration
5
- # (e.g., @dump_method, @load_method). These are set once and not mutated
6
- # at runtime, so thread safety is not a concern for this feature.
4
+ # Class instance variables are used here for configuration. These are set
5
+ # once at loadtime and not mutated, so thread safety is not an issue here.
7
6
  #
8
7
  module Familia
9
8
  module Features
@@ -1,6 +1,7 @@
1
1
  # lib/familia/features/transient_fields.rb
2
2
 
3
3
  require_relative 'transient_fields/redacted_string'
4
+ require_relative 'transient_fields/transient_field_type'
4
5
 
5
6
  module Familia
6
7
  module Features
@@ -104,7 +105,7 @@ module Familia
104
105
  # (HashiCorp Vault, AWS Secrets Manager) or languages with secure memory handling.
105
106
  #
106
107
  module TransientFields
107
- Familia::Base.add_feature self, :transient_fields, depends_on: nil
108
+ Familia::Base.add_feature self, :transient_fields, depends_on: nil, field_group: :transient_fields
108
109
 
109
110
  def self.included(base)
110
111
  Familia.trace :LOADED, self, base if Familia.debug?
@@ -143,8 +144,12 @@ module Familia
143
144
  @transient_fields ||= []
144
145
  @transient_fields << name unless @transient_fields.include?(name)
145
146
 
147
+ # Add to field_groups if the group exists
148
+ if field_groups&.key?(:transient_fields)
149
+ field_groups[:transient_fields] << name
150
+ end
151
+
146
152
  # Use the field type system for proper integration
147
- require_relative 'transient_fields/transient_field_type'
148
153
  field_type = TransientFieldType.new(name, as: as, **kwargs.merge(fast_method: false))
149
154
  register_field_type(field_type)
150
155
  end
@@ -4,7 +4,7 @@
4
4
  require_relative 'features/autoloader'
5
5
 
6
6
  module Familia
7
- FeatureDefinition = Data.define(:name, :depends_on)
7
+ FeatureDefinition = Data.define(:name, :depends_on, :field_group)
8
8
 
9
9
  # Familia::Features
10
10
  #
@@ -147,6 +147,11 @@ module Familia
147
147
  calling_location = caller_locations(1, 1)&.first
148
148
  options[:calling_location] = calling_location&.path
149
149
 
150
+ # Initialize field group if feature declares one
151
+ if feature_def&.field_group && respond_to?(:field_group)
152
+ field_group(feature_def.field_group)
153
+ end
154
+
150
155
  # Add feature options if the class supports them (Horreum classes)
151
156
  add_feature_options(feature_name, **options) if respond_to?(:add_feature_options)
152
157
 
@@ -117,25 +117,7 @@ module Familia
117
117
  klass.define_method :"#{method_name}=" do |value|
118
118
  instance_variable_set(:"@#{field_name}", value)
119
119
 
120
- # If this field is the identifier and object_identifier feature is loaded,
121
- # update objid_lookup mapping when identifier is set after objid generation
122
- if respond_to?(:objid) &&
123
- self.class.respond_to?(:identifier_field) &&
124
- self.class.identifier_field == field_name &&
125
- self.class.respond_to?(:objid_lookup)
126
- current_objid = instance_variable_get(:@objid)
127
- self.class.objid_lookup[current_objid] = value if current_objid && value
128
- end
129
120
 
130
- # If this field is the identifier and external_identifier feature is loaded,
131
- # update extid_lookup mapping when identifier is set after extid generation
132
- if respond_to?(:extid) &&
133
- self.class.respond_to?(:identifier_field) &&
134
- self.class.identifier_field == field_name &&
135
- self.class.respond_to?(:extid_lookup)
136
- current_extid = instance_variable_get(:@extid)
137
- self.class.extid_lookup[current_extid] = value if current_extid && value
138
- end
139
121
  end
140
122
  end
141
123
  end
@@ -149,6 +149,7 @@ module Familia
149
149
  # @see MultiResult For details on the return value structure
150
150
  # @see #batch_update For similar atomic field updates with MultiResult
151
151
  def transaction(&)
152
+ ensure_relatives_initialized!
152
153
  Familia::Connection::TransactionCore.execute_transaction(-> { dbclient }, &)
153
154
  end
154
155
  alias multi transaction
@@ -243,12 +244,32 @@ module Familia
243
244
  # @see MultiResult For details on the return value structure
244
245
  # @see Familia.transaction For atomic command execution
245
246
  def pipelined(&block)
247
+ ensure_relatives_initialized!
246
248
  Familia::Connection::PipelineCore.execute_pipeline(-> { dbclient }, &block)
247
249
  end
248
250
  alias pipeline pipelined
249
251
 
250
252
  private
251
253
 
254
+ # Ensures that related fields have been initialized before entering transactions or pipelines.
255
+ #
256
+ # This prevents Redis::Future errors when lazy initialization would occur inside
257
+ # transaction/pipeline blocks. When commands execute inside transactions, Redis returns
258
+ # Future objects that don't respond to standard methods, causing cryptic NoMethodError.
259
+ #
260
+ # @raise [RuntimeError] if instance has relations but they haven't been initialized
261
+ # @note Skips check for class methods - they create temporary instances internally
262
+ # @note Uses singleton class to avoid polluting instance variables
263
+ def ensure_relatives_initialized!
264
+ return if is_a?(Class) # Class methods handle their own instances
265
+ return unless self.class.respond_to?(:relations?) && self.class.relations?
266
+ return if singleton_class.instance_variable_defined?(:"@relatives_initialized")
267
+
268
+ raise "#{self.class} has related fields but they haven't been initialized. " \
269
+ "Did you override initialize without calling super? " \
270
+ "Related fields: #{self.class.related_fields.keys.join(', ')}"
271
+ end
272
+
252
273
  # Builds the class-level connection chain with handlers in priority order
253
274
  def build_connection_chain
254
275
  # Cache handlers at class level to avoid creating new instances per model instance
@@ -121,7 +121,7 @@ module Familia
121
121
  end
122
122
 
123
123
  def hmset(hsh = {})
124
- hsh ||= to_h
124
+ hsh ||= to_h_for_storage
125
125
  Familia.trace :HMSET, nil, hsh if Familia.debug?
126
126
  dbclient.hmset dbkey(suffix), hsh
127
127
  end
@@ -1,7 +1,8 @@
1
- # lib/familia/horreum/subclass/definition.rb
1
+ # lib/familia/horreum/definition.rb
2
2
 
3
- require_relative 'related_fields_management'
4
- require_relative '../shared/settings'
3
+ require_relative 'settings'
4
+
5
+ require_relative '../field_type'
5
6
 
6
7
  module Familia
7
8
  VALID_STRATEGIES = %i[raise skip ignore warn overwrite].freeze
@@ -28,9 +29,9 @@ module Familia
28
29
  @related_fields = nil
29
30
  @default_expiration = nil
30
31
 
31
- # Serialization settings
32
- @dump_method = nil
33
- @load_method = nil
32
+ # Field groups
33
+ @field_groups = nil
34
+ @current_field_group = nil
34
35
 
35
36
  # DefinitionMethods - Class-level DSL methods for defining Horreum model structure
36
37
  #
@@ -48,6 +49,84 @@ module Familia
48
49
  include Familia::Settings
49
50
  include Familia::Horreum::RelatedFieldsManagement # Provides DataType field methods
50
51
 
52
+ # Defines a field group to organize related fields.
53
+ #
54
+ # Field groups provide a way to categorize and query fields by purpose or feature.
55
+ # When a block is provided, fields defined within the block are automatically
56
+ # added to the group. Without a block, an empty group is initialized.
57
+ #
58
+ # @param name [Symbol, String] the name of the field group
59
+ # @yield optional block for defining fields within the group
60
+ # @return [Array<Symbol>] the array of field names in the group
61
+ #
62
+ # @raise [Familia::Problem] if attempting to nest field groups
63
+ #
64
+ # @example Manual field grouping
65
+ # class User < Familia::Horreum
66
+ # field_group :personal_info do
67
+ # field :name
68
+ # field :email
69
+ # end
70
+ # end
71
+ #
72
+ # User.personal_info # => [:name, :email]
73
+ #
74
+ # @example Initialize empty group
75
+ # class User < Familia::Horreum
76
+ # field_group :placeholder
77
+ # end
78
+ #
79
+ # User.placeholder # => []
80
+ #
81
+ def field_group(name, &block)
82
+
83
+ # Prevent nested field groups
84
+ if @current_field_group
85
+ raise Familia::Problem,
86
+ "Cannot define field group :#{name} while :#{@current_field_group} is being defined. " \
87
+ "Nested field groups are not supported."
88
+ end
89
+
90
+ # Initialize group
91
+ field_groups[name.to_sym] ||= []
92
+
93
+ if block_given?
94
+ @current_field_group = name.to_sym
95
+ begin
96
+ instance_eval(&block)
97
+ ensure
98
+ @current_field_group = nil
99
+ end
100
+ else
101
+ Familia.ld "[field_group] Created field group :#{name} but no block given" if Familia.debug?
102
+ end
103
+
104
+ field_groups[name.to_sym]
105
+ end
106
+
107
+ # Returns the list of all field group names defined for the class.
108
+ #
109
+ # @return [Array<Symbol>] array of field group names
110
+ #
111
+ # @example
112
+ # class User < Familia::Horreum
113
+ # field_group :personal_info do
114
+ # field :name
115
+ # end
116
+ # field_group :metadata do
117
+ # field :created_at
118
+ # end
119
+ # end
120
+ #
121
+ # User.field_groups # => [
122
+ # :personal_info => [...],
123
+ # :metadata => [..]
124
+ # ]
125
+ #
126
+ def field_groups
127
+ @field_groups ||= {}
128
+ end
129
+
51
130
  # Sets or retrieves the unique identifier field for the class.
52
131
  #
53
132
  # This method defines or returns the field or method that contains the unique
@@ -90,30 +169,9 @@ module Familia
90
169
  # - :skip - skip definition if method exists
91
170
  # - :warn - warn but proceed (may overwrite)
92
171
  # - :ignore - proceed silently (may overwrite)
93
- # @param category [Symbol, nil] field category for special handling:
94
- # - nil - regular field (default)
95
- # - :encrypted - field contains encrypted data
96
- # - :transient - field is not persisted
97
- # - Others, depending on features available
98
- #
99
- def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, category: nil)
100
- # Use field type system for consistency
101
- require_relative '../../field_type'
102
-
103
- # Create appropriate field type based on category
104
- field_type = if category == :transient
105
- require_relative '../../features/transient_fields/transient_field_type'
106
- TransientFieldType.new(name, as: as, fast_method: false, on_conflict: on_conflict)
107
- else
108
- # For regular fields and other categories, create custom field type with category override
109
- custom_field_type = Class.new(FieldType) do
110
- define_method :category do
111
- category || :field
112
- end
113
- end
114
- custom_field_type.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
115
- end
116
-
172
+ #
173
+ def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise)
174
+ field_type = FieldType.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
117
175
  register_field_type(field_type)
118
176
  end
119
177
 
@@ -123,8 +181,8 @@ module Familia
123
181
  # @param blk [Proc] a block that returns the suffix (optional).
124
182
  # @return [String, Symbol] the current suffix or Familia.default_suffix if none is set.
125
183
  #
126
- def suffix(a = nil, &blk)
127
- @suffix = a || blk if a || !blk.nil?
184
+ def suffix(val = nil, &blk)
185
+ @suffix = val || blk if val || !blk.nil?
128
186
  @suffix || Familia.default_suffix
129
187
  end
130
188
 
@@ -137,8 +195,8 @@ module Familia
137
195
  # which typically occurs with anonymous classes that haven't had their prefix
138
196
  # explicitly set.
139
197
  #
140
- def prefix(a = nil)
141
- @prefix = a if a
198
+ def prefix(val = nil)
199
+ @prefix = val if val
142
200
  @prefix || begin
143
201
  if name.nil?
144
202
  raise Problem, 'Cannot generate prefix for anonymous class. ' \
@@ -148,9 +206,9 @@ module Familia
148
206
  end
149
207
  end
150
208
 
151
- def logical_database(v = nil)
152
- Familia.trace :LOGICAL_DATABASE_DEF, "instvar:#{@logical_database}", v if Familia.debug?
153
- @logical_database = v unless v.nil?
209
+ def logical_database(num = nil)
210
+ Familia.trace :LOGICAL_DATABASE_DEF, "instvar:#{@logical_database}", num if Familia.debug?
211
+ @logical_database = num unless num.nil?
154
212
  @logical_database || parent&.logical_database
155
213
  end
156
214
 
@@ -175,14 +233,6 @@ module Familia
175
233
  @has_related_fields ||= false
176
234
  end
177
235
 
178
- def dump_method
179
- @dump_method || :to_json # Familia.dump_method
180
- end
181
-
182
- def load_method
183
- @load_method || :from_json # Familia.load_method
184
- end
185
-
186
236
  # Storage for field type instances
187
237
  def field_types
188
238
  @field_types ||= {}
@@ -221,6 +271,12 @@ module Familia
221
271
  # Complete the registration after installation. If we do this beforehand
222
272
  # we can run into issues where it looks like it's already installed.
223
273
  field_types[field_type.name] = field_type
274
+
275
+ # Add to current field group if one is active
276
+ if @current_field_group
277
+ @field_groups[@current_field_group] << field_type.name
278
+ end
279
+
224
280
  # Freeze the field_type to ensure immutability (maintains Data class heritage)
225
281
  field_type.freeze
226
282
  end
@@ -303,16 +359,6 @@ module Familia
303
359
  @feature_options[feature_name.to_sym]
304
360
  end
305
361
 
306
- # Create and register a transient field type
307
- #
308
- # @param name [Symbol] The field name
309
- #
310
- def transient_field(name, **)
311
- require_relative '../../features/transient_fields/transient_field_type'
312
- field_type = TransientFieldType.new(name, **, fast_method: false)
313
- register_field_type(field_type)
314
- end
315
-
316
362
  private
317
363
 
318
364
  # Hook to detect silent overwrites and handle conflicts
@@ -429,7 +475,7 @@ module Familia
429
475
 
430
476
  # Convert the provided value to a format suitable for Database storage.
431
477
  prepared = serialize_value(val)
432
- Familia.ld "[.define_fast_writer_method] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
478
+ Familia.ld "[define_fast_writer_method] #{fast_method_name} val: #{val.class} prepared: #{prepared.class}"
433
479
 
434
480
  # Use the existing accessor method to set the attribute value.
435
481
  send :"#{method_name}=", val
@@ -1,6 +1,4 @@
1
- # lib/familia/horreum/subclass/management.rb
2
-
3
- require_relative 'related_fields_management'
1
+ # lib/familia/horreum/management.rb
4
2
 
5
3
  module Familia
6
4
  class Horreum
@@ -127,15 +125,15 @@ module Familia
127
125
  # User.find_by_key("user:123") # Returns a User instance if it exists,
128
126
  # nil otherwise
129
127
  #
130
- def find_by_key(objkey)
128
+ def find_by_dbkey(objkey)
131
129
  raise ArgumentError, 'Empty key' if objkey.to_s.empty?
132
130
 
133
131
  # We use a lower-level method here b/c we're working with the
134
132
  # full key and not just the identifier.
135
133
  does_exist = dbclient.exists(objkey).positive?
136
134
 
137
- Familia.ld "[.find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
138
- Familia.trace :FROM_KEY, nil, objkey if Familia.debug?
135
+ Familia.ld "[find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
136
+ Familia.trace :FIND_BY_DBKEY_KEY, nil, objkey
139
137
 
140
138
  # This is the reason for calling exists first. We want to definitively
141
139
  # and without any ambiguity know if the object exists in the database. If it
@@ -145,11 +143,16 @@ module Familia
145
143
  return unless does_exist
146
144
 
147
145
  obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
148
- Familia.trace :FROM_KEY2, nil, "#{objkey}: #{obj.inspect}" if Familia.debug?
149
-
150
- new(**obj)
146
+ Familia.trace :FIND_BY_DBKEY_INSPECT, nil, "#{objkey}: #{obj.inspect}"
147
+
148
+ # Create instance and deserialize fields using existing helper method
149
+ # This avoids duplicating deserialization logic and keeps field-by-field processing
150
+ instance = allocate
151
+ instance.send(:initialize_relatives)
152
+ instance.send(:initialize_with_keyword_args_deserialize_value, **obj)
153
+ instance
151
154
  end
152
- alias from_dbkey find_by_key # deprecated
155
+ alias find_by_key find_by_dbkey
153
156
 
154
157
  # Retrieves and instantiates an object from Database using its identifier.
155
158
  #
@@ -170,19 +173,19 @@ module Familia
170
173
  # @example
171
174
  # User.find_by_id(123) # Equivalent to User.find_by_key("user:123:object")
172
175
  #
173
- def find_by_id(identifier, suffix = nil)
176
+ def find_by_identifier(identifier, suffix = nil)
174
177
  suffix ||= self.suffix
175
178
  return nil if identifier.to_s.empty?
176
179
 
177
180
  objkey = dbkey(identifier, suffix)
178
181
 
179
- Familia.ld "[.find_by_id] #{self} from key #{objkey})"
182
+ Familia.ld "[find_by_id] #{self} from key #{objkey})"
180
183
  Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
181
- find_by_key objkey
184
+ find_by_dbkey objkey
182
185
  end
186
+ alias find_by_id find_by_identifier
183
187
  alias find find_by_id
184
- alias load find_by_id # deprecated
185
- alias from_identifier find_by_id # deprecated
188
+ alias load find_by_id
186
189
 
187
190
  # Checks if an object with the given identifier exists in the database.
188
191
  #