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
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Benchmark: Field deserialization strategies in find_by_dbkey
4
+ #
5
+ # Compares different approaches to deserializing Redis hash values
6
+ # when loading Horreum objects from the database.
7
+ #
8
+ # Usage:
9
+ #
10
+ # $ ruby try/support/benchmarks/deserialization_benchmark.rb
11
+ # ======================================================================
12
+ # RESULTS SUMMARY (baseline: direct assignment)
13
+ # ======================================================================
14
+ #
15
+ # Baseline (no deserialization): 0.151312s
16
+ #
17
+ # Deserialization strategies:
18
+ # 1. Selective (only JSON-like strings): 0.174016s (1.15x baseline, +15.0% overhead) 🏆 FASTEST
19
+ # 2. Bulk JSON round-trip (parse/dump): 0.183926s (1.22x baseline, +21.6% overhead)
20
+ # 3. Cached instance + transform: 0.320223s (2.12x baseline, +111.6% overhead)
21
+ # 4. Current (field-by-field transform_values): 0.49568s (3.28x baseline, +227.6% overhead)
22
+
23
+ require_relative '../../../lib/familia'
24
+ require 'benchmark'
25
+ require 'json'
26
+
27
+ # Setup Redis connection
28
+ Familia.uri = ENV['REDIS_URI'] || 'redis://localhost:2525/3'
29
+
30
+ # Sample model with various field types
31
+ class BenchmarkUser < Familia::Horreum
32
+ identifier_field :user_id
33
+
34
+ field :user_id
35
+ field :name
36
+ field :email
37
+ field :age
38
+ field :active
39
+ field :metadata # Will store JSON hash
40
+ field :tags # Will store JSON array
41
+ field :created_at # Will store timestamp
42
+ field :score # Will store float
43
+ field :simple_string
44
+ end
45
+
46
+ # Create sample data with realistic values
47
+ sample_data = {
48
+ 'user_id' => 'user_12345',
49
+ 'name' => 'John Doe',
50
+ 'email' => 'john.doe@example.com',
51
+ 'age' => '35',
52
+ 'active' => 'true',
53
+ 'metadata' => '{"role":"admin","department":"engineering","level":5}',
54
+ 'tags' => '["ruby","redis","performance","optimization"]',
55
+ 'created_at' => Time.now.to_i.to_s,
56
+ 'score' => '98.7',
57
+ 'simple_string' => 'Just a plain string value',
58
+ }
59
+
60
+ # Persist sample data to Redis
61
+ user = BenchmarkUser.new(**sample_data)
62
+ user.save
63
+
64
+ # Get the raw hash data directly from Redis (what find_by_dbkey gets)
65
+ raw_hash = BenchmarkUser.dbclient.hgetall(user.dbkey)
66
+
67
+ puts 'Benchmarking deserialization strategies'
68
+ puts "Sample data fields: #{raw_hash.keys.size}"
69
+ puts "Raw hash: #{raw_hash.inspect}"
70
+ puts "\n"
71
+
72
+ # Strategy 1: Current field-by-field with transform_values
73
+ def strategy_current(fields, klass)
74
+ deserialized = fields.transform_values { |value| klass.new.deserialize_value(value) }
75
+ klass.new(**deserialized)
76
+ end
77
+
78
+ # Strategy 2: Bulk JSON round-trip
79
+ def strategy_bulk_json(fields, klass)
80
+ parsed = JSON.parse(JSON.dump(fields))
81
+ klass.new(**parsed)
82
+ end
83
+
84
+ # Strategy 3: Direct assignment without deserialization
85
+ def strategy_direct(fields, klass)
86
+ klass.new(**fields)
87
+ end
88
+
89
+ # Strategy 4: Selective deserialization (only JSON-looking strings)
90
+ def strategy_selective(fields, klass)
91
+ deserialized = fields.transform_values do |value|
92
+ if value.to_s.start_with?('{', '[')
93
+ begin
94
+ JSON.parse(value, symbolize_names: true)
95
+ rescue JSON::ParserError
96
+ value
97
+ end
98
+ else
99
+ value
100
+ end
101
+ end
102
+ klass.new(**deserialized)
103
+ end
104
+
105
+ # Strategy 5: Cached instance + transform
106
+ def strategy_cached_instance(fields, klass)
107
+ instance = klass.new
108
+ deserialized = fields.transform_values { |value| instance.deserialize_value(value) }
109
+ klass.new(**deserialized)
110
+ end
111
+
112
+ iterations = 10_000
113
+
114
+ puts "Running #{iterations} iterations per strategy...\n\n"
115
+
116
+ strategies = {
117
+ 'Current (field-by-field transform_values)' => :strategy_current,
118
+ 'Bulk JSON round-trip (parse/dump)' => :strategy_bulk_json,
119
+ 'Direct (no deserialization)' => :strategy_direct,
120
+ 'Selective (only parse JSON-like strings)' => :strategy_selective,
121
+ 'Cached instance + transform' => :strategy_cached_instance,
122
+ }
123
+
124
+ results = {}
125
+
126
+ strategies.each do |name, method_name|
127
+ time = Benchmark.measure do
128
+ iterations.times do
129
+ send(method_name, raw_hash, BenchmarkUser)
130
+ end
131
+ end
132
+ results[name] = time.real
133
+ puts "#{name}: #{time.real.round(6)} seconds (#{(iterations / time.real).round(0)} ops/sec)"
134
+ end
135
+
136
+ puts "\n" + ('=' * 70)
137
+ puts 'RESULTS SUMMARY (baseline: direct assignment)'
138
+ puts '=' * 70
139
+
140
+ # Use direct assignment as baseline (obviously fastest but incorrect)
141
+ baseline = results['Direct (no deserialization)']
142
+
143
+ # Sort deserialization strategies only (exclude baseline)
144
+ deserialization_strategies = results.reject { |name, _| name == 'Direct (no deserialization)' }
145
+ sorted = deserialization_strategies.sort_by { |_, time| time }
146
+
147
+ puts "
148
+ Baseline (no deserialization): #{baseline.round(6)}s"
149
+ puts "
150
+ Deserialization strategies:
151
+ "
152
+
153
+ sorted.each_with_index do |(name, time), index|
154
+ overhead = ((time / baseline - 1) * 100).round(1)
155
+ vs_baseline = (time / baseline).round(2)
156
+ marker = index == 0 ? '🏆 FASTEST' : ''
157
+ puts "#{index + 1}. #{name}: #{time.round(6)}s (#{vs_baseline}x baseline, +#{overhead}% overhead) #{marker}"
158
+ end
159
+
160
+ puts "
161
+ " + ('=' * 70)
162
+ puts 'RECOMMENDATIONS'
163
+ puts '=' * 70
164
+
165
+ puts 'Best strategy depends on your data:'
166
+ puts ' • Mostly simple strings → Direct or Selective'
167
+ puts ' • Mixed types with JSON → Current (field-by-field)'
168
+ puts ' • Heavy JSON payloads → Consider lazy deserialization'
169
+
170
+ # Cleanup
171
+ user.destroy!
172
+
173
+ __END__
174
+
175
+ # Example output expectations:
176
+ #
177
+ # Current approach should be moderately fast
178
+ # Bulk JSON round-trip should be slower (extra serialization step)
179
+ # Direct assignment should be fastest but incorrect for complex types
180
+ # Selective should be fast for simple data, slower for JSON-heavy data
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Correctness Test: Field deserialization strategies
4
+ #
5
+ # Verifies that different deserialization approaches produce identical
6
+ # results for all field types (strings, numbers, JSON, nested structures).
7
+ #
8
+ # Usage:
9
+ # $ try/support/benchmarks/deserialization_correctness_test.rb
10
+ #
11
+
12
+ require_relative '../../../lib/familia'
13
+ require 'json'
14
+
15
+ # Setup Redis connection
16
+ Familia.uri = ENV['REDIS_URI'] || 'redis://localhost:2525/3'
17
+
18
+ # Sample model with various field types
19
+ class CorrectnessTestUser < Familia::Horreum
20
+ identifier_field :user_id
21
+
22
+ field :user_id
23
+ field :name
24
+ field :email
25
+ field :age
26
+ field :active
27
+ field :metadata # Will store JSON hash
28
+ field :tags # Will store JSON array
29
+ field :created_at # Will store timestamp
30
+ field :score # Will store float
31
+ field :simple_string
32
+ field :nested_data # Will store deeply nested JSON
33
+ field :empty_string
34
+ field :nil_value
35
+ end
36
+
37
+ # Create sample data with comprehensive test cases
38
+ sample_data = {
39
+ 'user_id' => 'user_12345',
40
+ 'name' => 'John Doe',
41
+ 'email' => 'john.doe@example.com',
42
+ 'age' => '35',
43
+ 'active' => 'true',
44
+ 'metadata' => '{"role":"admin","department":"engineering","level":5}',
45
+ 'tags' => '["ruby","redis","performance","optimization"]',
46
+ 'created_at' => Time.now.to_i.to_s,
47
+ 'score' => '98.7',
48
+ 'simple_string' => 'Just a plain string value',
49
+ 'nested_data' => '{"user":{"profile":{"settings":{"theme":"dark","notifications":true}}}}',
50
+ 'empty_string' => '',
51
+ 'nil_value' => nil,
52
+ }
53
+
54
+ # Persist sample data to Redis
55
+ user = CorrectnessTestUser.new(**sample_data)
56
+ user.save
57
+
58
+ # Get the raw hash data directly from Redis
59
+ raw_hash = CorrectnessTestUser.dbclient.hgetall(user.dbkey)
60
+
61
+ puts 'Correctness Test: Deserialization Strategies'
62
+ puts '=' * 70
63
+ puts "\nTesting with #{raw_hash.keys.size} fields"
64
+ puts "\n"
65
+
66
+ # Strategy 1: Current field-by-field (reference implementation)
67
+ def strategy_current(fields, klass)
68
+ deserialized = fields.transform_values { |value| klass.new.deserialize_value(value) }
69
+ klass.new(**deserialized)
70
+ end
71
+
72
+ # Strategy 2: Bulk JSON round-trip
73
+ def strategy_bulk_json(fields, klass)
74
+ parsed = JSON.parse(JSON.dump(fields))
75
+ klass.new(**parsed)
76
+ end
77
+
78
+ # Strategy 3: Selective deserialization (only JSON-looking strings)
79
+ def strategy_selective(fields, klass)
80
+ deserialized = fields.transform_values do |value|
81
+ if value.to_s.start_with?('{', '[')
82
+ begin
83
+ JSON.parse(value, symbolize_names: true)
84
+ rescue JSON::ParserError
85
+ value
86
+ end
87
+ else
88
+ value
89
+ end
90
+ end
91
+ klass.new(**deserialized)
92
+ end
93
+
94
+ # Create objects using each strategy
95
+ current_obj = strategy_current(raw_hash, CorrectnessTestUser)
96
+ bulk_obj = strategy_bulk_json(raw_hash, CorrectnessTestUser)
97
+ selective_obj = strategy_selective(raw_hash, CorrectnessTestUser)
98
+
99
+ # Test helper to compare field values
100
+ def compare_values(field, current_val, test_val, strategy_name)
101
+ current_class = current_val.class
102
+ test_class = test_val.class
103
+
104
+ if current_val == test_val && current_class == test_class
105
+ { status: :pass, field: field, strategy: strategy_name }
106
+ else
107
+ {
108
+ status: :fail,
109
+ field: field,
110
+ strategy: strategy_name,
111
+ current: { value: current_val.inspect, class: current_class },
112
+ test: { value: test_val.inspect, class: test_class },
113
+ }
114
+ end
115
+ end
116
+
117
+ # Run correctness tests
118
+ results = []
119
+ fields_to_test = CorrectnessTestUser.fields
120
+
121
+ puts "Testing #{fields_to_test.size} fields across strategies...\n\n"
122
+
123
+ # Test Bulk JSON strategy
124
+ puts 'Strategy: Bulk JSON round-trip'
125
+ puts '-' * 70
126
+ fields_to_test.each do |field|
127
+ current_val = current_obj.send(field)
128
+ test_val = bulk_obj.send(field)
129
+ result = compare_values(field, current_val, test_val, 'Bulk JSON')
130
+ results << result
131
+
132
+ if result[:status] == :pass
133
+ puts " ✓ #{field}: #{test_val.inspect} (#{test_val.class})"
134
+ else
135
+ puts " ✗ #{field}: MISMATCH"
136
+ puts " Current: #{result[:current][:value]} (#{result[:current][:class]})"
137
+ puts " Bulk: #{result[:test][:value]} (#{result[:test][:class]})"
138
+ end
139
+ end
140
+
141
+ puts "\n"
142
+
143
+ # Test Selective strategy
144
+ puts 'Strategy: Selective (only JSON-like strings)'
145
+ puts '-' * 70
146
+ fields_to_test.each do |field|
147
+ current_val = current_obj.send(field)
148
+ test_val = selective_obj.send(field)
149
+ result = compare_values(field, current_val, test_val, 'Selective')
150
+ results << result
151
+
152
+ if result[:status] == :pass
153
+ puts " ✓ #{field}: #{test_val.inspect} (#{test_val.class})"
154
+ else
155
+ puts " ✗ #{field}: MISMATCH"
156
+ puts " Current: #{result[:current][:value]} (#{result[:current][:class]})"
157
+ puts " Selective: #{result[:test][:value]} (#{result[:test][:class]})"
158
+ end
159
+ end
160
+
161
+ puts "\n" + ('=' * 70)
162
+ puts 'SUMMARY'
163
+ puts '=' * 70
164
+
165
+ # Group results by strategy
166
+ by_strategy = results.group_by { |r| r[:strategy] }
167
+
168
+ by_strategy.each do |strategy, strategy_results|
169
+ passed = strategy_results.count { |r| r[:status] == :pass }
170
+ failed = strategy_results.count { |r| r[:status] == :fail }
171
+ total = strategy_results.size
172
+
173
+ status_icon = failed == 0 ? '✓' : '✗'
174
+ puts "\n#{status_icon} #{strategy}: #{passed}/#{total} passed"
175
+
176
+ next unless failed > 0
177
+
178
+ puts ' Failed fields:'
179
+ strategy_results.select { |r| r[:status] == :fail }.each do |result|
180
+ puts " - #{result[:field]}"
181
+ end
182
+ end
183
+
184
+ # Overall assessment
185
+ all_passed = results.all? { |r| r[:status] == :pass }
186
+
187
+ puts "\n" + ('=' * 70)
188
+ puts 'VERDICT'
189
+ puts '=' * 70
190
+
191
+ if all_passed
192
+ puts '✓ All strategies produce identical results to current implementation'
193
+ puts ' Safe to use for optimization'
194
+ else
195
+ puts '✗ Some strategies produce different results'
196
+ puts ' Review failures before implementing'
197
+ end
198
+
199
+ puts "\n" + ('=' * 70)
200
+ puts 'RECOMMENDATIONS'
201
+ puts '=' * 70
202
+
203
+ # Analyze which strategies passed
204
+ bulk_passed = by_strategy['Bulk JSON'].all? { |r| r[:status] == :pass }
205
+ selective_passed = by_strategy['Selective'].all? { |r| r[:status] == :pass }
206
+
207
+ if bulk_passed && selective_passed
208
+ puts '✓ Both Bulk JSON and Selective strategies are correct'
209
+ puts ' → Use Selective for best performance (+15-18% overhead)'
210
+ puts ' → Use Bulk JSON for simplicity (+20% overhead)'
211
+ elsif bulk_passed
212
+ puts '✓ Bulk JSON strategy is correct'
213
+ puts ' → Safe to implement (+20% overhead vs baseline)'
214
+ elsif selective_passed
215
+ puts '✓ Selective strategy is correct'
216
+ puts ' → Safe to implement (+15-18% overhead vs baseline)'
217
+ else
218
+ puts '✗ No alternative strategies passed all tests'
219
+ puts ' → Stick with current implementation'
220
+ puts ' → Or fix identified issues in failing strategies'
221
+ end
222
+
223
+ # Cleanup
224
+ user.destroy!
225
+
226
+ __END__
227
+
228
+ # Expected output:
229
+ #
230
+ # All strategies should pass if they correctly handle:
231
+ # - Simple strings (no parsing needed)
232
+ # - JSON objects (hashes)
233
+ # - JSON arrays
234
+ # - Numbers as strings
235
+ # - Empty strings
236
+ # - Nil values
237
+ # - Nested JSON structures
@@ -6,12 +6,21 @@
6
6
 
7
7
  require 'digest'
8
8
 
9
- require_relative '../../lib/familia'
9
+ require_relative '../../../lib/familia'
10
10
 
11
11
  Familia.enable_database_logging = true
12
12
  Familia.enable_database_counter = true
13
13
  Familia.uri = 'redis://127.0.0.1:2525'
14
14
 
15
+ def generate_random_email
16
+ # Generate a random username
17
+ username = (0...8).map { ('a'..'z').to_a[rand(26)] }.join
18
+ # Define a domain
19
+ domain = "example.com"
20
+ # Combine to form an email address
21
+ "#{username}@#{domain}"
22
+ end
23
+
15
24
  class Bone < Familia::Horreum
16
25
  using Familia::Refinements::TimeLiterals
17
26
 
@@ -194,7 +203,7 @@ end
194
203
  #
195
204
  # NOTE: This will do nothing unless RedactedString is already requried
196
205
  unless defined?(RedactedString)
197
- require_relative '../../lib/familia/features/transient_fields/redacted_string'
206
+ require_relative '../../../lib/familia/features/transient_fields/redacted_string'
198
207
  end
199
208
  module RedactedStringTestHelper
200
209
  refine RedactedString do
@@ -206,7 +215,7 @@ module RedactedStringTestHelper
206
215
  end
207
216
 
208
217
  unless defined?(SingleUseRedactedString)
209
- require_relative '../../lib/familia/features/transient_fields/single_use_redacted_string'
218
+ require_relative '../../../lib/familia/features/transient_fields/single_use_redacted_string'
210
219
  end
211
220
  module SingleUseRedactedStringTestHelper
212
221
  refine SingleUseRedactedString do
@@ -19,7 +19,7 @@
19
19
  # the directory patterns that autoloader.included generates and also the
20
20
  # exclusion logic works correctly.
21
21
 
22
- require_relative '../../lib/familia'
22
+ require_relative '../../../lib/familia'
23
23
  require 'fileutils'
24
24
  require 'tmpdir'
25
25
 
@@ -1,6 +1,6 @@
1
1
  # try/core/base_enhancements_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -97,14 +97,6 @@ EmptyBaseTest.respond_to?(:feature)
97
97
  Familia::Base.features_available
98
98
  #=:> Hash
99
99
 
100
- ## Dump and load methods are set
101
- Familia::Base.dump_method
102
- #=> :to_json
103
-
104
- ## Load method is set correctly
105
- Familia::Base.load_method
106
- #=> :from_json
107
-
108
100
  ## Base module provides inspect with class name
109
101
  @base_uuid.inspect.include?('BaseUuidTest')
110
102
  #=> true
@@ -1,6 +1,6 @@
1
1
  # try/core/connection_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -1,6 +1,6 @@
1
1
  # try/core/errors_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6
 
@@ -1,4 +1,4 @@
1
- require_relative '../helpers/test_helpers'
1
+ require_relative '../../support/helpers/test_helpers'
2
2
 
3
3
  module RefinedContext
4
4
  using Familia::Refinements::TimeLiterals
@@ -0,0 +1,110 @@
1
+ # try/unit/core/familia_logger_try.rb
2
+
3
+ require_relative '../../support/helpers/test_helpers'
4
+ require 'logger'
5
+ require 'stringio'
6
+
7
+ ## FamiliaLogger has trace method
8
+ logger = Familia::FamiliaLogger.new(StringIO.new)
9
+ logger.respond_to?(:trace)
10
+ #=> true
11
+
12
+ ## trace method logs with TRACE level
13
+ output = StringIO.new
14
+ logger = Familia::FamiliaLogger.new(output)
15
+ logger.level = Familia::FamiliaLogger::TRACE
16
+ logger.trace('Test message')
17
+ output.string
18
+ #=~> /Test message/
19
+
20
+ ## FamiliaLogger has TRACE constant
21
+ Familia::FamiliaLogger::TRACE
22
+ #=> 0
23
+
24
+ ## trace method accepts progname parameter
25
+ output = StringIO.new
26
+ logger = Familia::FamiliaLogger.new(output)
27
+ logger.level = Familia::FamiliaLogger::TRACE
28
+ logger.trace('MyApp') { 'Test message' }
29
+ output.string
30
+ #=~> /MyApp/
31
+ #=~> /Test message/
32
+
33
+ ## trace method accepts block for message
34
+ output = StringIO.new
35
+ logger = Familia::FamiliaLogger.new(output)
36
+ logger.level = Familia::FamiliaLogger::TRACE
37
+ logger.trace { 'Block message' }
38
+ output.string
39
+ #=~> /Block message/
40
+
41
+ ## LogFormatter properly formats TRACE messages
42
+ output = StringIO.new
43
+ logger = Familia::FamiliaLogger.new(output)
44
+ logger.level = Familia::FamiliaLogger::TRACE
45
+ logger.formatter = Familia::LogFormatter.new
46
+ logger.trace('Trace test')
47
+ output.string
48
+ #=~> /^T,/
49
+
50
+ ## Nested trace calls preserve TRACE context
51
+ output = StringIO.new
52
+ logger = Familia::FamiliaLogger.new(output)
53
+ logger.level = Familia::FamiliaLogger::TRACE
54
+ logger.formatter = Familia::LogFormatter.new
55
+ logger.trace do
56
+ logger.trace('Inner trace message')
57
+ 'Outer trace message'
58
+ end
59
+ output.string.lines.size
60
+ #=> 2
61
+
62
+ ## Nested trace inner message formatted as TRACE
63
+ output = StringIO.new
64
+ logger = Familia::FamiliaLogger.new(output)
65
+ logger.level = Familia::FamiliaLogger::TRACE
66
+ logger.formatter = Familia::LogFormatter.new
67
+ logger.trace do
68
+ logger.trace('Inner trace message')
69
+ 'Outer trace message'
70
+ end
71
+ output.string.lines[0]
72
+ #=~> /^T,.*Inner trace message/
73
+
74
+ ## Nested trace outer message formatted as TRACE
75
+ output = StringIO.new
76
+ logger = Familia::FamiliaLogger.new(output)
77
+ logger.level = Familia::FamiliaLogger::TRACE
78
+ logger.formatter = Familia::LogFormatter.new
79
+ logger.trace do
80
+ logger.trace('Inner trace message')
81
+ 'Outer trace message'
82
+ end
83
+ output.string.lines[1]
84
+ #=~> /^T,.*Outer trace message/
85
+
86
+ ## Nested trace calls clean up context for subsequent debug calls
87
+ output = StringIO.new
88
+ logger = Familia::FamiliaLogger.new(output)
89
+ logger.level = Familia::FamiliaLogger::TRACE
90
+ logger.formatter = Familia::LogFormatter.new
91
+ logger.trace do
92
+ logger.trace('Inner trace')
93
+ 'Outer trace'
94
+ end
95
+ logger.debug('After nested traces')
96
+ output.string.lines.size
97
+ #=> 3
98
+
99
+ ## Debug call after nested traces formatted as DEBUG not TRACE
100
+ output = StringIO.new
101
+ logger = Familia::FamiliaLogger.new(output)
102
+ logger.level = Familia::FamiliaLogger::TRACE
103
+ logger.formatter = Familia::LogFormatter.new
104
+ logger.trace do
105
+ logger.trace('Inner trace')
106
+ 'Outer trace'
107
+ end
108
+ logger.debug('After nested traces')
109
+ output.string.lines[2]
110
+ #=~> /^D,.*After nested traces/
@@ -1,6 +1,6 @@
1
1
  # try/core/familia_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  ## Check for help class
6
6
  Bone.related_fields.keys # consistent b/c hashes are ordered
@@ -3,7 +3,7 @@
3
3
  # Test Valkey/Redis middleware components
4
4
  # Mock Valkey/Redis client with middleware for testing
5
5
 
6
- require_relative '../helpers/test_helpers'
6
+ require_relative '../../support/helpers/test_helpers'
7
7
 
8
8
  class MockDatabase
9
9
  attr_reader :logged_commands
@@ -27,6 +27,46 @@ class MockDatabase
27
27
  end
28
28
  end
29
29
 
30
+ ## increment_middleware_version! increases version counter
31
+ initial_version = Familia.middleware_version
32
+ Familia.increment_middleware_version!
33
+ Familia.middleware_version > initial_version
34
+ #=> true
35
+
36
+ ## increment_middleware_version! increments by exactly 1
37
+ initial_version = Familia.middleware_version
38
+ Familia.increment_middleware_version!
39
+ Familia.middleware_version - initial_version
40
+ #=> 1
41
+
42
+ ## fiber_connection= stores connection with current version
43
+ mock_connection = "test_connection"
44
+ Familia.fiber_connection=(mock_connection)
45
+ stored = Fiber[:familia_connection]
46
+ [stored[0], stored[1] == Familia.middleware_version]
47
+ #=> ["test_connection", true]
48
+
49
+ ## fiber_connection= updates version when middleware version changes
50
+ mock_connection = "test_connection"
51
+ Familia.fiber_connection=(mock_connection)
52
+ old_version = Fiber[:familia_connection][1]
53
+ Familia.increment_middleware_version!
54
+ Familia.fiber_connection=(mock_connection)
55
+ new_version = Fiber[:familia_connection][1]
56
+ new_version > old_version
57
+ #=> true
58
+
59
+ ## clear_fiber_connection! removes fiber-local connection
60
+ Familia.fiber_connection=("test_connection")
61
+ Familia.clear_fiber_connection!
62
+ Fiber[:familia_connection]
63
+ #=> nil
64
+
65
+ ## clear_fiber_connection! is safe when no connection exists
66
+ Familia.clear_fiber_connection!
67
+ Fiber[:familia_connection]
68
+ #=> nil
69
+
30
70
  ## MockDatabase can log commands with timing
31
71
  dbclient = MockDatabase.new
32
72
  result = dbclient.get("test_key")
@@ -1,6 +1,6 @@
1
1
  # try/core/settings_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  Familia.debug = false
6
6