familia 2.0.0.pre17 → 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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +60 -0
  3. data/CLAUDE.md +9 -2
  4. data/Gemfile.lock +1 -1
  5. data/README.md +13 -0
  6. data/bin/irb +1 -1
  7. data/docs/guides/core-field-system.md +48 -26
  8. data/docs/migrating/v2.0.0-pre18.md +58 -0
  9. data/docs/qodo-merge-compliance.md +96 -0
  10. data/lib/familia/base.rb +0 -2
  11. data/lib/familia/connection/middleware.rb +58 -4
  12. data/lib/familia/connection.rb +1 -1
  13. data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
  14. data/lib/familia/data_type/serialization.rb +5 -5
  15. data/lib/familia/data_type.rb +2 -2
  16. data/lib/familia/encryption/encrypted_data.rb +12 -2
  17. data/lib/familia/encryption/manager.rb +11 -4
  18. data/lib/familia/features/autoloader.rb +3 -1
  19. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  20. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
  21. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +41 -27
  22. data/lib/familia/features/safe_dump.rb +2 -3
  23. data/lib/familia/horreum/database_commands.rb +1 -1
  24. data/lib/familia/horreum/definition.rb +6 -37
  25. data/lib/familia/horreum/management.rb +17 -12
  26. data/lib/familia/horreum/persistence.rb +1 -1
  27. data/lib/familia/horreum/serialization.rb +91 -73
  28. data/lib/familia/horreum.rb +10 -6
  29. data/lib/familia/identifier_extractor.rb +60 -0
  30. data/lib/familia/logging.rb +271 -112
  31. data/lib/familia/refinements.rb +0 -1
  32. data/lib/familia/version.rb +1 -1
  33. data/lib/familia.rb +2 -2
  34. data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
  35. data/pr_agent.toml +31 -0
  36. data/pr_compliance_checklist.yaml +45 -0
  37. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  38. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  39. data/try/edge_cases/json_serialization_try.rb +2 -2
  40. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  41. data/try/edge_cases/race_conditions_try.rb +1 -1
  42. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  43. data/try/edge_cases/string_coercion_try.rb +1 -1
  44. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  45. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  46. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  47. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  48. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  49. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  50. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  51. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  52. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  53. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  54. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  55. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  56. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  57. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  58. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  59. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  60. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  61. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  62. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  63. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  64. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  65. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  66. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  67. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  68. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  69. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  70. data/try/features/expiration/expiration_try.rb +1 -1
  71. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  72. data/try/features/feature_dependencies_try.rb +1 -1
  73. data/try/features/feature_improvements_try.rb +1 -1
  74. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  75. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  76. data/try/features/quantization/quantization_try.rb +1 -1
  77. data/try/features/real_feature_integration_try.rb +17 -14
  78. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  79. data/try/features/relationships/indexing_try.rb +6 -1
  80. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  81. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  82. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  83. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  84. data/try/features/relationships/relationships_api_changes_try.rb +1 -1
  85. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  86. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  87. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  88. data/try/features/relationships/relationships_performance_try.rb +1 -1
  89. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  90. data/try/features/relationships/relationships_try.rb +1 -1
  91. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  92. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  93. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  94. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  95. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  96. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  97. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  98. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
  99. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  100. data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
  101. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  102. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
  103. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
  104. data/try/{core → integration/connection}/pools_try.rb +1 -1
  105. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  106. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  107. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  108. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  109. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  110. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  111. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  112. data/try/{core → integration}/create_method_try.rb +1 -1
  113. data/try/integration/cross_component_try.rb +1 -1
  114. data/try/{core → integration}/database_consistency_try.rb +11 -8
  115. data/try/{core → integration}/familia_extended_try.rb +1 -1
  116. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  117. data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
  118. data/try/{models → integration/models}/customer_try.rb +1 -1
  119. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  120. data/try/{models → integration/models}/familia_object_try.rb +1 -1
  121. data/try/{core → integration}/persistence_operations_try.rb +1 -1
  122. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  123. data/try/{configuration → integration}/scenarios_try.rb +1 -1
  124. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  125. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  126. data/try/performance/benchmarks_try.rb +2 -2
  127. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  128. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  129. data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
  130. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  131. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  132. data/try/{core → unit/core}/connection_try.rb +1 -1
  133. data/try/{core → unit/core}/errors_try.rb +1 -1
  134. data/try/{core → unit/core}/extensions_try.rb +1 -1
  135. data/try/unit/core/familia_logger_try.rb +110 -0
  136. data/try/{core → unit/core}/familia_try.rb +1 -1
  137. data/try/{core → unit/core}/middleware_try.rb +41 -1
  138. data/try/{core → unit/core}/settings_try.rb +1 -1
  139. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  140. data/try/{core → unit/core}/tools_try.rb +1 -1
  141. data/try/{core → unit/core}/utils_try.rb +17 -14
  142. data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
  143. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  144. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  145. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  146. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  147. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  148. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  149. data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
  150. data/try/{data_types → unit/data_types}/string_try.rb +1 -1
  151. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  152. data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +1 -1
  153. data/try/{horreum → unit/horreum}/base_try.rb +3 -3
  154. data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
  155. data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
  156. data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
  157. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
  158. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  159. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  160. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  161. data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
  162. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  163. data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
  164. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  165. data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
  166. data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
  167. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  168. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  169. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  170. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  171. metadata +134 -125
  172. data/lib/familia/distinguisher.rb +0 -85
  173. data/lib/familia/refinements/logger_trace.rb +0 -60
  174. data/try/refinements/logger_trace_methods_try.rb +0 -44
  175. /data/try/{debugging → support/debugging}/README.md +0 -0
  176. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  177. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  178. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  179. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  180. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  181. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  182. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  183. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  184. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  185. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  186. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  187. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  188. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  189. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  190. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  191. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  192. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  193. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  194. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  195. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  196. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  197. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  198. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  199. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  200. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  201. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  202. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  203. /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
  204. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  205. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  206. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  207. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  208. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  209. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  210. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  211. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  212. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  213. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  214. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  215. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  216. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  217. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  218. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  219. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  220. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -28,7 +28,9 @@ module Familia::Features
28
28
  ]
29
29
 
30
30
  # Ensure the Features module exists within the base module
31
- base.const_set(:Features, Module.new) unless base.const_defined?(:Features) || config_name.eql?('features')
31
+ unless base.const_defined?(:Features) || config_name.eql?('features')
32
+ base.const_set(:Features, Module.new)
33
+ end
32
34
 
33
35
  # Use the shared autoload_files method
34
36
  autoload_files(dir_patterns, log_prefix: "Autoloader[#{config_name}]")
@@ -29,8 +29,10 @@ module Familia
29
29
  # Already concealed, store as-is
30
30
  instance_variable_set(:"@#{field_name}", value)
31
31
  elsif field_type.encrypted_json?(value)
32
- # Already encrypted JSON from database - wrap in ConcealedString without re-encrypting
33
- concealed = ConcealedString.new(value, self, field_type)
32
+ # Already encrypted (JSON string or Hash from database) - wrap in ConcealedString without re-encrypting
33
+ # Convert Hash back to JSON string if needed (v2.0 deserialization returns Hash)
34
+ encrypted_string = value.is_a?(Hash) ? Familia::JsonSerializer.dump(value) : value
35
+ concealed = ConcealedString.new(encrypted_string, self, field_type)
34
36
  instance_variable_set(:"@#{field_name}", concealed)
35
37
  else
36
38
  # Encrypt plaintext and wrap in ConcealedString
@@ -138,7 +140,13 @@ module Familia
138
140
 
139
141
  # Check if a string looks like encrypted JSON data
140
142
  def encrypted_json?(data)
141
- Familia::Encryption::EncryptedData.valid?(data)
143
+ # Support both JSON strings (legacy) and Hashes (v2.0 deserialization)
144
+ if data.is_a?(Hash)
145
+ required_keys = %w[algorithm nonce ciphertext auth_tag key_version]
146
+ required_keys.all? { |key| data.key?(key) || data.key?(key.to_sym) }
147
+ else
148
+ Familia::Encryption::EncryptedData.valid?(data)
149
+ end
142
150
  end
143
151
 
144
152
  private
@@ -75,7 +75,7 @@ module Familia
75
75
  actual_target_class.class_eval do
76
76
  # Helper method to get index set for a specific field value
77
77
  # This acts as a factory for field-value-specific DataTypes
78
- define_method("#{index_name}_for") do |field_value|
78
+ define_method(:"#{index_name}_for") do |field_value|
79
79
  # Return properly managed DataType instance with parameterized key
80
80
  index_key = "#{index_name}:#{field_value}"
81
81
  Familia::UnsortedSet.new(index_key, parent: self)
@@ -99,26 +99,26 @@ module Familia
99
99
  # Generate instance sampling method (e.g., company.sample_from_department)
100
100
  actual_target_class.class_eval do
101
101
 
102
- define_method("sample_from_#{field}") do |field_value, count = 1|
102
+ define_method(:"sample_from_#{field}") do |field_value, count = 1|
103
103
  index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
104
104
 
105
105
  # Get random members efficiently (O(1) via SRANDMEMBER with count)
106
106
  # Returns array even for count=1 for consistent API
107
107
  index_set.sample(count).map do |id|
108
- indexed_class.new(index_set.deserialize_value(id))
108
+ indexed_class.find_by_identifier(id)
109
109
  end
110
110
  end
111
111
 
112
112
  # Generate bulk query method (e.g., company.find_all_by_department)
113
- define_method("find_all_by_#{field}") do |field_value|
113
+ define_method(:"find_all_by_#{field}") do |field_value|
114
114
  index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
115
115
 
116
116
  # Get all members from set
117
- index_set.members.map { |id| indexed_class.new(id) }
117
+ index_set.members.map { |id| indexed_class.find_by_identifier(id) }
118
118
  end
119
119
 
120
120
  # Generate method to rebuild the index for this parent instance
121
- define_method("rebuild_#{index_name}") do
121
+ define_method(:"rebuild_#{index_name}") do
122
122
  # This would need to be implemented based on how you track which
123
123
  # objects belong to this parent instance
124
124
  # For now, just a placeholder
@@ -138,7 +138,7 @@ module Familia
138
138
  def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
139
139
  target_class_config = target_class.config_name
140
140
  indexed_class.class_eval do
141
- method_name = "add_to_#{target_class_config}_#{index_name}"
141
+ method_name = :"add_to_#{target_class_config}_#{index_name}"
142
142
  Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
143
143
 
144
144
  define_method(method_name) do |target_instance|
@@ -154,7 +154,7 @@ module Familia
154
154
  index_set.add(identifier)
155
155
  end
156
156
 
157
- method_name = "remove_from_#{target_class_config}_#{index_name}"
157
+ method_name = :"remove_from_#{target_class_config}_#{index_name}"
158
158
  Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
159
159
 
160
160
  define_method(method_name) do |target_instance|
@@ -170,7 +170,7 @@ module Familia
170
170
  index_set.remove(identifier)
171
171
  end
172
172
 
173
- method_name = "update_in_#{target_class_config}_#{index_name}"
173
+ method_name = :"update_in_#{target_class_config}_#{index_name}"
174
174
  Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
175
175
 
176
176
  define_method(method_name) do |target_instance, old_field_value = nil|
@@ -104,36 +104,42 @@ module Familia
104
104
 
105
105
  # Generate instance query method (e.g., company.find_by_badge_number)
106
106
  actual_target_class.class_eval do
107
- define_method("find_by_#{field}") do |field_value|
107
+ define_method(:"find_by_#{field}") do |provided_value|
108
108
  # Use declared field accessor instead of manual instantiation
109
109
  index_hash = send(index_name)
110
110
 
111
- # Get the identifier from the hash
112
- object_id = index_hash[field_value.to_s]
113
- 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
114
118
 
115
- indexed_class.new(object_id)
119
+ indexed_class.find_by_identifier(record_id)
116
120
  end
117
121
 
118
122
  # Generate bulk query method (e.g., company.find_all_by_badge_number)
119
- define_method("find_all_by_#{field}") do |field_values|
120
- field_values = Array(field_values)
121
- 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?
122
126
 
123
127
  # Use declared field accessor instead of manual instantiation
124
128
  index_hash = send(index_name)
125
129
 
126
130
  # Get all identifiers from the hash
127
- object_ids = index_hash.values_at(*field_values.map(&:to_s))
131
+ record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
128
132
  # Filter out nil values and instantiate objects
129
- 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
+ }
130
136
  end
131
137
 
132
138
  # Accessor method already created by ensure_index_field above
133
139
  # No need to manually define it here
134
140
 
135
141
  # Generate method to rebuild the unique index for this parent instance
136
- define_method("rebuild_#{index_name}") do
142
+ define_method(:"rebuild_#{index_name}") do
137
143
  # Use declared field accessor instead of manual instantiation
138
144
  index_hash = send(index_name)
139
145
 
@@ -160,7 +166,7 @@ module Familia
160
166
  def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
161
167
  target_class_config = target_class.config_name
162
168
  indexed_class.class_eval do
163
- method_name = "add_to_#{target_class_config}_#{index_name}"
169
+ method_name = :"add_to_#{target_class_config}_#{index_name}"
164
170
  Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
165
171
 
166
172
  define_method(method_name) do |target_instance|
@@ -176,7 +182,7 @@ module Familia
176
182
  index_hash[field_value.to_s] = identifier
177
183
  end
178
184
 
179
- method_name = "remove_from_#{target_class_config}_#{index_name}"
185
+ method_name = :"remove_from_#{target_class_config}_#{index_name}"
180
186
  Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
181
187
 
182
188
  define_method(method_name) do |target_instance|
@@ -192,7 +198,7 @@ module Familia
192
198
  index_hash.remove(field_value.to_s)
193
199
  end
194
200
 
195
- method_name = "update_in_#{target_class_config}_#{index_name}"
201
+ method_name = :"update_in_#{target_class_config}_#{index_name}"
196
202
  Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
197
203
 
198
204
  define_method(method_name) do |target_instance, old_field_value = nil|
@@ -222,31 +228,39 @@ module Familia
222
228
  # - Employee.email_index
223
229
  # - Employee.rebuild_email_index
224
230
  def generate_query_methods_class(field, index_name, indexed_class)
225
- indexed_class.define_singleton_method("find_by_#{field}") do |field_value|
231
+ indexed_class.define_singleton_method(:"find_by_#{field}") do |provided_id|
226
232
  index_hash = send(index_name) # Access the class-level hashkey DataType
227
- object_id = index_hash[field_value.to_s]
228
233
 
229
- 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)
230
240
 
231
- new(object_id)
241
+ return nil unless record_id
242
+
243
+ indexed_class.find_by_identifier(record_id)
232
244
  end
233
245
 
234
246
  # Generate class-level bulk query method
235
- indexed_class.define_singleton_method("find_all_by_#{field}") do |field_values|
236
- field_values = Array(field_values)
237
- 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?
238
250
 
239
251
  index_hash = send(index_name) # Access the class-level hashkey DataType
240
- object_ids = index_hash.values_at(*field_values.map(&:to_s))
252
+ record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
241
253
  # Filter out nil values and instantiate objects
242
- 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
+ }
243
257
  end
244
258
 
245
259
  # The index accessor method is already created by the class_hashkey declaration
246
260
  # No need to manually create it - Horreum handles this automatically
247
261
 
248
262
  # Generate method to rebuild the class-level index
249
- indexed_class.define_singleton_method("rebuild_#{index_name}") do
263
+ indexed_class.define_singleton_method(:"rebuild_#{index_name}") do
250
264
  index_hash = send(index_name) # Access the class-level hashkey DataType
251
265
 
252
266
  # Clear existing index using DataType method
@@ -265,7 +279,7 @@ module Familia
265
279
  # - employee.update_in_class_email_index(old_email)
266
280
  def generate_mutation_methods_class(field, index_name, indexed_class)
267
281
  indexed_class.class_eval do
268
- define_method("add_to_class_#{index_name}") do
282
+ define_method(:"add_to_class_#{index_name}") do
269
283
  index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
270
284
  field_value = send(field)
271
285
 
@@ -274,7 +288,7 @@ module Familia
274
288
  index_hash[field_value.to_s] = identifier
275
289
  end
276
290
 
277
- define_method("remove_from_class_#{index_name}") do
291
+ define_method(:"remove_from_class_#{index_name}") do
278
292
  index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
279
293
  field_value = send(field)
280
294
 
@@ -283,7 +297,7 @@ module Familia
283
297
  index_hash.remove(field_value.to_s)
284
298
  end
285
299
 
286
- 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|
287
301
  new_field_value = send(field)
288
302
 
289
303
  # Use class-level transaction for atomicity with DataType abstraction
@@ -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
@@ -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
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative 'settings'
4
4
 
5
+ require_relative '../field_type'
6
+
5
7
  module Familia
6
8
  VALID_STRATEGIES = %i[raise skip ignore warn overwrite].freeze
7
9
 
@@ -27,10 +29,6 @@ module Familia
27
29
  @related_fields = nil
28
30
  @default_expiration = nil
29
31
 
30
- # Serialization settings
31
- @dump_method = nil
32
- @load_method = nil
33
-
34
32
  # Field groups
35
33
  @field_groups = nil
36
34
  @current_field_group = nil
@@ -171,30 +169,9 @@ module Familia
171
169
  # - :skip - skip definition if method exists
172
170
  # - :warn - warn but proceed (may overwrite)
173
171
  # - :ignore - proceed silently (may overwrite)
174
- # @param category [Symbol, nil] field category for special handling:
175
- # - nil - regular field (default)
176
- # - :encrypted - field contains encrypted data
177
- # - :transient - field is not persisted
178
- # - Others, depending on features available
179
- #
180
- def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, category: nil)
181
- # Use field type system for consistency
182
- require_relative '../field_type'
183
-
184
- # Create appropriate field type based on category
185
- field_type = if category == :transient
186
- require_relative '../features/transient_fields/transient_field_type'
187
- TransientFieldType.new(name, as: as, fast_method: false, on_conflict: on_conflict)
188
- else
189
- # For regular fields and other categories, create custom field type with category override
190
- custom_field_type = Class.new(FieldType) do
191
- define_method :category do
192
- category || :field
193
- end
194
- end
195
- custom_field_type.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
196
- end
197
-
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)
198
175
  register_field_type(field_type)
199
176
  end
200
177
 
@@ -256,14 +233,6 @@ module Familia
256
233
  @has_related_fields ||= false
257
234
  end
258
235
 
259
- def dump_method
260
- @dump_method || :to_json # Familia.dump_method
261
- end
262
-
263
- def load_method
264
- @load_method || :from_json # Familia.load_method
265
- end
266
-
267
236
  # Storage for field type instances
268
237
  def field_types
269
238
  @field_types ||= {}
@@ -506,7 +475,7 @@ module Familia
506
475
 
507
476
  # Convert the provided value to a format suitable for Database storage.
508
477
  prepared = serialize_value(val)
509
- 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}"
510
479
 
511
480
  # Use the existing accessor method to set the attribute value.
512
481
  send :"#{method_name}=", val
@@ -125,15 +125,15 @@ module Familia
125
125
  # User.find_by_key("user:123") # Returns a User instance if it exists,
126
126
  # nil otherwise
127
127
  #
128
- def find_by_key(objkey)
128
+ def find_by_dbkey(objkey)
129
129
  raise ArgumentError, 'Empty key' if objkey.to_s.empty?
130
130
 
131
131
  # We use a lower-level method here b/c we're working with the
132
132
  # full key and not just the identifier.
133
133
  does_exist = dbclient.exists(objkey).positive?
134
134
 
135
- Familia.ld "[.find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
136
- 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
137
137
 
138
138
  # This is the reason for calling exists first. We want to definitively
139
139
  # and without any ambiguity know if the object exists in the database. If it
@@ -143,11 +143,16 @@ module Familia
143
143
  return unless does_exist
144
144
 
145
145
  obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
146
- Familia.trace :FROM_KEY2, nil, "#{objkey}: #{obj.inspect}" if Familia.debug?
147
-
148
- 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
149
154
  end
150
- alias from_dbkey find_by_key # deprecated
155
+ alias find_by_key find_by_dbkey
151
156
 
152
157
  # Retrieves and instantiates an object from Database using its identifier.
153
158
  #
@@ -168,19 +173,19 @@ module Familia
168
173
  # @example
169
174
  # User.find_by_id(123) # Equivalent to User.find_by_key("user:123:object")
170
175
  #
171
- def find_by_id(identifier, suffix = nil)
176
+ def find_by_identifier(identifier, suffix = nil)
172
177
  suffix ||= self.suffix
173
178
  return nil if identifier.to_s.empty?
174
179
 
175
180
  objkey = dbkey(identifier, suffix)
176
181
 
177
- Familia.ld "[.find_by_id] #{self} from key #{objkey})"
182
+ Familia.ld "[find_by_id] #{self} from key #{objkey})"
178
183
  Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
179
- find_by_key objkey
184
+ find_by_dbkey objkey
180
185
  end
186
+ alias find_by_id find_by_identifier
181
187
  alias find find_by_id
182
- alias load find_by_id # deprecated
183
- alias from_identifier find_by_id # deprecated
188
+ alias load find_by_id
184
189
 
185
190
  # Checks if an object with the given identifier exists in the database.
186
191
  #
@@ -354,7 +354,7 @@ module Familia
354
354
  # their uninitialized state during refresh operations
355
355
  reset_transient_fields!
356
356
 
357
- optimistic_refresh(**fields)
357
+ naive_refresh(**fields)
358
358
  end
359
359
 
360
360
  # Refreshes object state from the DB and returns self for method chaining.