familia 2.0.0.pre17 → 2.0.0.pre19

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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +118 -6
  3. data/CLAUDE.md +43 -11
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +9 -47
  6. data/README.md +52 -0
  7. data/bin/irb +1 -1
  8. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
  9. data/changelog.d/20251011_203905_delano_next.rst +30 -0
  10. data/changelog.d/20251011_212633_delano_next.rst +13 -0
  11. data/changelog.d/20251011_221253_delano_next.rst +26 -0
  12. data/docs/guides/core-field-system.md +48 -26
  13. data/docs/guides/feature-expiration.md +18 -18
  14. data/docs/migrating/v2.0.0-pre18.md +58 -0
  15. data/docs/migrating/v2.0.0-pre19.md +197 -0
  16. data/docs/qodo-merge-compliance.md +96 -0
  17. data/examples/datatype_standalone.rb +281 -0
  18. data/lib/familia/base.rb +0 -2
  19. data/lib/familia/connection/behavior.rb +252 -0
  20. data/lib/familia/connection/handlers.rb +95 -0
  21. data/lib/familia/connection/middleware.rb +58 -4
  22. data/lib/familia/connection/operation_core.rb +1 -1
  23. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
  24. data/lib/familia/connection/transaction_core.rb +7 -9
  25. data/lib/familia/connection.rb +2 -1
  26. data/lib/familia/data_type/connection.rb +151 -7
  27. data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
  28. data/lib/familia/data_type/serialization.rb +9 -5
  29. data/lib/familia/data_type/types/hashkey.rb +1 -1
  30. data/lib/familia/data_type.rb +2 -2
  31. data/lib/familia/encryption/encrypted_data.rb +12 -2
  32. data/lib/familia/encryption/manager.rb +11 -4
  33. data/lib/familia/errors.rb +51 -14
  34. data/lib/familia/features/autoloader.rb +3 -1
  35. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  36. data/lib/familia/features/expiration/extensions.rb +8 -10
  37. data/lib/familia/features/expiration.rb +19 -19
  38. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
  39. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
  40. data/lib/familia/features/relationships/indexing.rb +37 -42
  41. data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
  42. data/lib/familia/features/safe_dump.rb +2 -3
  43. data/lib/familia/field_type.rb +2 -1
  44. data/lib/familia/horreum/connection.rb +11 -35
  45. data/lib/familia/horreum/database_commands.rb +130 -11
  46. data/lib/familia/horreum/definition.rb +8 -38
  47. data/lib/familia/horreum/management.rb +38 -27
  48. data/lib/familia/horreum/persistence.rb +191 -67
  49. data/lib/familia/horreum/serialization.rb +94 -73
  50. data/lib/familia/horreum/utils.rb +0 -8
  51. data/lib/familia/horreum.rb +41 -18
  52. data/lib/familia/identifier_extractor.rb +60 -0
  53. data/lib/familia/logging.rb +268 -112
  54. data/lib/familia/refinements.rb +0 -1
  55. data/lib/familia/settings.rb +7 -7
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -2
  58. data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
  59. data/pr_agent.toml +31 -0
  60. data/pr_compliance_checklist.yaml +45 -0
  61. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  62. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  63. data/try/edge_cases/json_serialization_try.rb +2 -2
  64. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  65. data/try/edge_cases/race_conditions_try.rb +1 -1
  66. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  67. data/try/edge_cases/string_coercion_try.rb +5 -5
  68. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  69. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  70. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  71. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  72. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  73. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  74. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  75. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  76. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  77. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  78. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  79. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  80. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  81. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  82. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  83. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  84. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  85. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  86. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  87. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  88. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  89. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  90. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  91. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  92. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  93. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  94. data/try/features/expiration/expiration_try.rb +2 -2
  95. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  96. data/try/features/feature_dependencies_try.rb +1 -1
  97. data/try/features/feature_improvements_try.rb +1 -1
  98. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  99. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  100. data/try/features/quantization/quantization_try.rb +1 -1
  101. data/try/features/real_feature_integration_try.rb +17 -14
  102. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  103. data/try/features/relationships/indexing_try.rb +34 -5
  104. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  105. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  106. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  107. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  108. data/try/features/relationships/relationships_api_changes_try.rb +5 -5
  109. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  110. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  111. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  112. data/try/features/relationships/relationships_performance_try.rb +1 -1
  113. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  114. data/try/features/relationships/relationships_try.rb +1 -1
  115. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  116. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  117. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  118. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  119. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  120. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  121. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  122. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
  123. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  124. data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
  125. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  126. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
  127. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
  128. data/try/{core → integration/connection}/pools_try.rb +1 -1
  129. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  130. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  131. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  132. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  133. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  134. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  135. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  136. data/try/{core → integration}/create_method_try.rb +23 -23
  137. data/try/integration/cross_component_try.rb +1 -1
  138. data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
  139. data/try/integration/data_types/datatype_transactions_try.rb +247 -0
  140. data/try/{core → integration}/database_consistency_try.rb +11 -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 +6 -2
  144. data/try/{models → integration/models}/customer_try.rb +1 -1
  145. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  146. data/try/{models → integration/models}/familia_object_try.rb +2 -2
  147. data/try/{core → integration}/persistence_operations_try.rb +163 -11
  148. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  149. data/try/{configuration → integration}/scenarios_try.rb +1 -1
  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 +12 -3
  156. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  157. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  158. data/try/{core → unit/core}/connection_try.rb +1 -1
  159. data/try/{core → unit/core}/errors_try.rb +1 -1
  160. data/try/{core → unit/core}/extensions_try.rb +1 -1
  161. data/try/unit/core/familia_logger_try.rb +110 -0
  162. data/try/{core → unit/core}/familia_try.rb +1 -1
  163. data/try/{core → unit/core}/middleware_try.rb +41 -1
  164. data/try/{core → unit/core}/settings_try.rb +1 -1
  165. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  166. data/try/{core → unit/core}/tools_try.rb +1 -1
  167. data/try/{core → unit/core}/utils_try.rb +17 -14
  168. data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
  169. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  170. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  171. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  172. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  173. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  174. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  175. data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
  176. data/try/{data_types → unit/data_types}/string_try.rb +2 -2
  177. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  178. data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
  179. data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
  180. data/try/{horreum → unit/horreum}/base_try.rb +4 -4
  181. data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
  182. data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
  183. data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
  184. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -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 +3 -3
  189. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  190. data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
  191. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  192. data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
  193. data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
  194. data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
  195. data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
  196. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  197. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  198. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  199. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  200. metadata +147 -126
  201. data/lib/familia/distinguisher.rb +0 -85
  202. data/lib/familia/refinements/logger_trace.rb +0 -60
  203. data/try/refinements/logger_trace_methods_try.rb +0 -44
  204. /data/try/{debugging → support/debugging}/README.md +0 -0
  205. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  206. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  207. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  208. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  209. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  210. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  211. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  212. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  213. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  214. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  215. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  216. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  217. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  218. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  219. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  220. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  221. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  222. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  223. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  224. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  225. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  226. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  227. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  228. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  229. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  230. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  231. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  232. /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
  233. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  234. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  235. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  236. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  237. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  238. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  239. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  240. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  241. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  242. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  243. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  244. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  245. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  246. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  247. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  248. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  249. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -0,0 +1,441 @@
1
+ # try/integration/relationships_persistence_round_trip_try.rb
2
+ #
3
+ # CRITICAL PRIORITY: Full Persistence Round-Trip Testing for Relationships
4
+ #
5
+ # PURPOSE:
6
+ # This test file addresses the critical gap that allowed the indexing bug to go undetected.
7
+ # Tests must verify that objects can be saved, indexed, found via relationships, and loaded
8
+ # with all fields intact - not just that the APIs work with in-memory objects.
9
+ #
10
+ # THE BUG PATTERN THIS EXPOSES:
11
+ # Previous tests created objects with .new(), added them to indexes/collections, and tested
12
+ # that the APIs worked - but never verified that find_by_* methods returned fully hydrated
13
+ # objects from the database. Production code failed because find_by_identifier calls hgetall
14
+ # which returns empty hashes for unsaved objects.
15
+ #
16
+ # BUGS DOCUMENTED IN THIS TEST:
17
+ # 1. SERIALIZATION BUG: All fields from Redis come back as strings
18
+ # - Integer 42 becomes "42"
19
+ # - Time timestamps become string representations
20
+ # - This breaks type expectations and equality comparisons
21
+ #
22
+ # 2. INCOMPLETE HYDRATION BUG: Multi-index queries return objects with only identifier
23
+ # - find_all_by_department returns RTPEmployee objects
24
+ # - But these objects only have @emp_id populated, email/department are nil
25
+ # - This suggests find_by_identifier isn't properly deserializing Redis hash fields
26
+ #
27
+ # 3. PARTICIPATION API BUG: Class-level participation methods have unclear signatures
28
+ # - add_to_class_all_domains method exists but requires unknown arguments
29
+ # - Documentation/API inconsistency prevents proper usage
30
+ #
31
+ # TESTING PHILOSOPHY - "The Persistence Contract":
32
+ # 1. Create object with .new() + field values
33
+ # 2. Save object with .save or .create
34
+ # 3. Index/Relate using relationship methods
35
+ # 4. Find via relationship query methods
36
+ # 5. Verify found object has ALL expected fields (not just identifier)
37
+ # 6. Modify object state
38
+ # 7. Re-save and verify updates persist
39
+ # 8. Destroy and verify cleanup
40
+ #
41
+ # TEST COVERAGE AREAS:
42
+ # - unique_index: Objects found via find_by_* should be fully hydrated (FAILS - serialization)
43
+ # - multi_index: Objects found via sample_from_*/find_all_by_* should be fully hydrated (FAILS - incomplete)
44
+ # - participates_in: Collection members should be loadable with all fields (PARTIAL)
45
+ # - Bulk operations: find_all_by_* should return fully hydrated objects (FAILS - incomplete)
46
+ # - Field equality: Loaded object fields should match original values (FAILS - serialization)
47
+ # - Nil fields vs missing fields: Correct handling after round-trip (FAILS - "" vs nil)
48
+ # - Type preservation: Field types should be preserved (FAILS - all strings)
49
+ #
50
+ # ANTI-PATTERNS THIS PREVENTS:
51
+ # - "Working by coincidence" - APIs work in memory but fail with persistence
52
+ # - Incomplete object loading - Objects with only identifier set
53
+ # - Silent field loss - Fields not persisted or not loaded
54
+ # - Type coercion bugs - String "123" becomes Integer 123 unexpectedly
55
+ #
56
+ # See: commit 802e80d0e5a0602e393468a9777b8e151ead11a6 for the bug this prevents
57
+
58
+ require_relative '../support/helpers/test_helpers'
59
+
60
+ # Test classes for persistence round-trip verification
61
+ class ::RTPUser < Familia::Horreum
62
+ feature :relationships
63
+
64
+ identifier_field :user_id
65
+ field :user_id
66
+ field :email
67
+ field :name
68
+ field :age
69
+ field :created_at
70
+
71
+ # Class-level unique indexing
72
+ unique_index :email, :email_index
73
+ unique_index :name, :name_index
74
+ end
75
+
76
+ class ::RTPCompany < Familia::Horreum
77
+ feature :relationships
78
+
79
+ identifier_field :company_id
80
+ field :company_id
81
+ field :name
82
+ field :industry
83
+ end
84
+
85
+ class ::RTPEmployee < Familia::Horreum
86
+ feature :relationships
87
+
88
+ identifier_field :emp_id
89
+ field :emp_id
90
+ field :email
91
+ field :department
92
+ field :badge_number
93
+ field :hire_date
94
+
95
+ # Instance-scoped unique indexing
96
+ unique_index :badge_number, :badge_index, within: RTPCompany
97
+
98
+ # Instance-scoped multi-value indexing
99
+ multi_index :department, :dept_index, within: RTPCompany
100
+ end
101
+
102
+ class ::RTPDomain < Familia::Horreum
103
+ feature :relationships
104
+
105
+ identifier_field :domain_id
106
+ field :domain_id
107
+ field :name
108
+ field :created_at
109
+
110
+ # Participation
111
+ participates_in RTPCompany, :domains, score: :created_at
112
+ class_participates_in :all_domains, score: :created_at
113
+ end
114
+
115
+ # Setup - create test data with known values
116
+ @test_user_id = "rtp_user_#{Familia.now.to_i}"
117
+ @test_email = "roundtrip@example.com"
118
+ @test_name = "Alice Roundtrip"
119
+ @test_age = 30
120
+
121
+ @test_company_id = "rtp_comp_#{Familia.now.to_i}"
122
+ @test_company_name = "Acme Corp"
123
+ @test_industry = "Technology"
124
+
125
+ @test_emp_id = "rtp_emp_#{Familia.now.to_i}"
126
+ @test_emp_email = "employee@acme.com"
127
+ @test_department = "engineering"
128
+ @test_badge = "BADGE_RTP_001"
129
+ @test_hire_date = Time.now.to_i
130
+
131
+ @test_domain_id = "rtp_dom_#{Familia.now.to_i}"
132
+ @test_domain_name = "example.com"
133
+ @test_domain_created = Familia.now.to_i
134
+
135
+ # =============================================
136
+ # 1. UNIQUE INDEX - Class-Level Round-Trip
137
+ # =============================================
138
+
139
+ ## Create user with all fields populated
140
+ @user = RTPUser.new(
141
+ user_id: @test_user_id,
142
+ email: @test_email,
143
+ name: @test_name,
144
+ age: @test_age,
145
+ created_at: Familia.now.to_i
146
+ )
147
+ @user.user_id
148
+ #=> @test_user_id
149
+
150
+ ## User does not exist before save
151
+ RTPUser.exists?(@test_user_id)
152
+ #=> false
153
+
154
+ ## Save user to database (CRITICAL STEP)
155
+ @user.save
156
+ #==> true
157
+
158
+ ## User exists after save
159
+ RTPUser.exists?(@test_user_id)
160
+ #=> true
161
+
162
+ ## Add user to class-level email index
163
+ @user.add_to_class_email_index
164
+ @user.add_to_class_name_index
165
+ RTPUser.email_index.hgetall[@test_email]
166
+ #=> @test_user_id
167
+
168
+ ## Find via unique_index returns fully hydrated object
169
+ @found_user = RTPUser.find_by_email(@test_email)
170
+ @found_user.class
171
+ #=> RTPUser
172
+
173
+ ## Found user has correct user_id
174
+ @found_user.user_id
175
+ #=> @test_user_id
176
+
177
+ ## Found user has correct email
178
+ @found_user.email
179
+ #=> @test_email
180
+
181
+ ## Found user has correct name
182
+ @found_user.name
183
+ #=> @test_name
184
+
185
+ ## Found user has correct age (Integer preserved)
186
+ @found_user.age
187
+ #=> @test_age
188
+
189
+ ## created_at is correctly deserialized as Integer
190
+ @found_user.created_at
191
+ #=:> Integer
192
+
193
+ ## All fields match original values (types preserved)
194
+ [@found_user.user_id, @found_user.email, @found_user.name, @found_user.age]
195
+ #=> [@user.user_id, @user.email, @user.name, @user.age]
196
+
197
+ ## Find via second index also returns fully hydrated object
198
+ @found_by_name = RTPUser.find_by_name(@test_name)
199
+ @found_by_name.email
200
+ #=> @test_email
201
+
202
+ ## Bulk find returns fully hydrated objects
203
+ @found_users = RTPUser.find_all_by_email([@test_email])
204
+ @found_users.first.name
205
+ #=> @test_name
206
+
207
+
208
+ # =============================================
209
+ # 2. UNIQUE INDEX - Instance-Scoped Round-Trip
210
+ # =============================================
211
+
212
+ ## Create company and save it
213
+ @company = RTPCompany.new(
214
+ company_id: @test_company_id,
215
+ name: @test_company_name,
216
+ industry: @test_industry
217
+ )
218
+ @company.save
219
+ RTPCompany.exists?(@test_company_id)
220
+ #=> true
221
+
222
+ ## Create employee and save it
223
+ @employee = RTPEmployee.new(
224
+ emp_id: @test_emp_id,
225
+ email: @test_emp_email,
226
+ department: @test_department,
227
+ badge_number: @test_badge,
228
+ hire_date: @test_hire_date
229
+ )
230
+ @employee.save
231
+ RTPEmployee.exists?(@test_emp_id)
232
+ #=> true
233
+
234
+ ## Add employee to company's badge index
235
+ @employee.add_to_rtp_company_badge_index(@company)
236
+ @company.badge_index.hgetall[@test_badge]
237
+ #=> @test_emp_id
238
+
239
+ ## Find employee via instance-scoped unique index
240
+ @found_emp = @company.find_by_badge_number(@test_badge)
241
+ @found_emp.class
242
+ #=> RTPEmployee
243
+
244
+ ## Found employee has identifier (works)
245
+ @found_emp.emp_id
246
+ #=> @test_emp_id
247
+
248
+ ## Found employee has correct email (string serialization)
249
+ @found_emp.email
250
+ #=> @test_emp_email
251
+
252
+ ## Found employee has correct department (string serialization)
253
+ @found_emp.department
254
+ #=> @test_department
255
+
256
+ ## Found employee has correct hire_date (Integer preserved)
257
+ @found_emp.hire_date
258
+ #=> @test_hire_date
259
+
260
+ ## Bulk query via instance-scoped index returns hydrated objects
261
+ @found_emps = @company.find_all_by_badge_number([@test_badge])
262
+ @found_emps.first.email
263
+ #=> @test_emp_email
264
+
265
+ # =============================================
266
+ # 3. MULTI-VALUE INDEX - Round-Trip
267
+ # =============================================
268
+
269
+ ## Add employee to department multi-value index
270
+ @employee.add_to_rtp_company_dept_index(@company)
271
+ @company.dept_index_for(@test_department).size
272
+ #=> 1
273
+
274
+ ## Sample from multi-index returns array of objects
275
+ @sampled = @company.sample_from_department(@test_department, 1)
276
+ @sampled.class
277
+ #=> Array
278
+
279
+ ## INCOMPLETE HYDRATION BUG: Sampled employee exists but missing fields
280
+ @sampled.first.class
281
+ #=> RTPEmployee
282
+
283
+ ## FIXED: Objects now fully hydrated with all fields
284
+ [@sampled.first.emp_id, @sampled.first.email, @sampled.first.department]
285
+ #=> [@test_emp_id, @test_emp_email, @test_department]
286
+
287
+ ## INCOMPLETE HYDRATION BUG: find_all_by returns objects missing fields
288
+ @dept_employees = @company.find_all_by_department(@test_department)
289
+ @dept_employees.length
290
+ #=> 1
291
+
292
+ ## Multiple employees in same department
293
+ @emp2_id = "rtp_emp2_#{Familia.now.to_i}"
294
+ @emp2_badge = "BADGE_RTP_002"
295
+ @emp2 = RTPEmployee.new(
296
+ emp_id: @emp2_id,
297
+ email: "emp2@acme.com",
298
+ department: @test_department,
299
+ badge_number: @emp2_badge
300
+ )
301
+ @emp2.save
302
+ @emp2.add_to_rtp_company_dept_index(@company)
303
+ @company.find_all_by_department(@test_department).length
304
+ #=> 2
305
+
306
+ ## FIXED: All multi-index objects fully hydrated with all fields
307
+ @all_eng = @company.find_all_by_department(@test_department)
308
+ @all_eng.all? { |e| e.is_a?(RTPEmployee) && e.emp_id && e.email }
309
+ #=> true
310
+
311
+ # =============================================
312
+ # 4. PARTICIPATION - Round-Trip
313
+ # =============================================
314
+
315
+ ## Create domain and save it
316
+ @domain = RTPDomain.new(
317
+ domain_id: @test_domain_id,
318
+ name: @test_domain_name,
319
+ created_at: @test_domain_created
320
+ )
321
+ @domain.save
322
+ RTPDomain.exists?(@test_domain_id)
323
+ #=> true
324
+
325
+ ## Add domain to company participation collection
326
+ @company.add_domain(@domain)
327
+ @company.domains.size
328
+ #=> 1
329
+
330
+ ## Domain appears in company collection
331
+ @company.domains.members.include?(@test_domain_id)
332
+ #=> true
333
+
334
+ ## Load domain from participation collection
335
+ @domain_ids = @company.domains.members
336
+ @loaded_domains = @domain_ids.map { |id| RTPDomain.find(id) }.compact
337
+ @loaded_domains.first.class
338
+ #=> RTPDomain
339
+
340
+ ## Loaded domain has all fields
341
+ @loaded_domains.first.domain_id
342
+ #=> @test_domain_id
343
+
344
+ ## Loaded domain has correct name
345
+ @loaded_domains.first.name
346
+ #=> @test_domain_name
347
+
348
+ ## Loaded domain has correct created_at (Integer preserved)
349
+ @loaded_domains.first.created_at
350
+ #=> @test_domain_created
351
+
352
+ ## Class-level participation requires manual addition - API unclear for add_to_class_all_domains
353
+ # Skip: @domain.add_to_class_all_domains - method signature unclear
354
+ RTPDomain.all_domains.size
355
+ #=> 0
356
+
357
+ ## Domain cannot be loaded from class collection (expected - not added)
358
+ @class_domain_ids = RTPDomain.all_domains.members
359
+ @loaded_from_class = @class_domain_ids.map { |id| RTPDomain.find(id) }.compact
360
+ @loaded_from_class.any? { |d| d.domain_id == @test_domain_id }
361
+ #=> false
362
+
363
+ # =============================================
364
+ # 5. UPDATE PERSISTENCE - Round-Trip
365
+ # =============================================
366
+
367
+ ## Modify user fields
368
+ @new_age = 31
369
+ @user.age = @new_age
370
+ @user.save
371
+ @user.age
372
+ #=> @new_age
373
+
374
+ ## Updated age correctly preserved as Integer
375
+ @reloaded_user = RTPUser.find_by_email(@test_email)
376
+ @reloaded_user.age
377
+ #=> @new_age
378
+
379
+ ## Update email and verify index updates
380
+ @new_email = "newemail@example.com"
381
+ @old_email = @user.email
382
+ @user.email = @new_email
383
+ @user.save
384
+ @user.update_in_class_email_index(@old_email)
385
+ RTPUser.find_by_email(@new_email)&.user_id
386
+ #=> @test_user_id
387
+
388
+ ## Old email no longer finds user
389
+ RTPUser.find_by_email(@old_email)
390
+ #=> nil
391
+
392
+ # =============================================
393
+ # 6. NIL FIELDS - Round-Trip
394
+ # =============================================
395
+
396
+ ## User with nil field saves correctly
397
+ @user_nil_age = RTPUser.new(
398
+ user_id: "rtp_nil_#{Familia.now.to_i}",
399
+ email: "nil@example.com",
400
+ name: "Nil Tester",
401
+ age: nil
402
+ )
403
+ @user_nil_age.save
404
+ @user_nil_age.age
405
+ #=> nil
406
+
407
+ ## Nil fields correctly preserved as nil (not empty string)
408
+ @reloaded_nil = RTPUser.find(@user_nil_age.user_id)
409
+ @reloaded_nil.age
410
+ #=> nil
411
+
412
+ ## Nil field vs missing field handled correctly, the field exists
413
+ @reloaded_nil.respond_to?(:age)
414
+ #=> true
415
+
416
+ ## Nil field vs missing field handled correctly, the field does not exist
417
+ @reloaded_nil.respond_to?(:plop)
418
+ #=> false
419
+
420
+ # =============================================
421
+ # 7. TYPE PRESERVATION - Round-Trip
422
+ # =============================================
423
+
424
+ ## Type preservation: Integer fields stay Integer after round-trip
425
+ @test_int_user = RTPUser.new(user_id: "rtp_int_#{Familia.now.to_i}", age: 42)
426
+ @test_int_user.save
427
+ @reloaded_int = RTPUser.find(@test_int_user.user_id)
428
+ @reloaded_int.age.class
429
+ #=> Integer
430
+
431
+ ## String fields work correctly (expected behavior)
432
+ @reloaded_int.user_id.class
433
+ #=> String
434
+
435
+ # =============================================
436
+ # Cleanup
437
+ # =============================================
438
+
439
+ [@user, @company, @employee, @emp2, @domain, @user_nil_age, @test_int_user, @found_user, @reloaded_user].each do |obj|
440
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
441
+ end
@@ -1,6 +1,6 @@
1
1
  # Comprehensive configuration scenarios
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
 
5
5
  ## multi-database configuration may fail
6
6
  begin
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Test Familia::SecureIdentifier methods
4
4
 
5
- require_relative '../helpers/test_helpers'
5
+ require_relative '../support/helpers/test_helpers'
6
6
 
7
7
  Familia.debug = false
8
8
 
@@ -1,6 +1,6 @@
1
1
  # try/core/verifiable_identifier_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
  require 'familia/verifiable_identifier'
5
5
 
6
6
  ## Module is available
@@ -1,6 +1,6 @@
1
1
  # Performance benchmarks separate from stress tests
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../support/helpers/test_helpers'
4
4
  require 'benchmark'
5
5
 
6
6
  ## serialization performance comparison
@@ -17,7 +17,7 @@ json_time = Benchmark.realtime do
17
17
  end
18
18
 
19
19
  familia_time = Benchmark.realtime do
20
- 100.times { Familia.distinguisher(large_data) }
20
+ 100.times { Familia::JsonSerializer.dump(large_data) }
21
21
  end
22
22
 
23
23
  json_time > 0 && familia_time > 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