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,376 @@
1
+ # Testing edge cases for unique index validation
2
+ # lib/familia/features/relationships/indexing/unique_index_generators.rb
3
+ # lib/familia/horreum/persistence.rb
4
+
5
+ require_relative '../../../lib/familia'
6
+
7
+ Familia.debug = false
8
+
9
+ # ========================================
10
+ # Setup: Define test models
11
+ # ========================================
12
+
13
+ class EdgeCaseCompany < Familia::Horreum
14
+ feature :relationships
15
+
16
+ identifier_field :company_id
17
+ field :company_id
18
+ field :company_name
19
+
20
+ # init receives no arguments - fields already set from new()
21
+ # Use ||= to apply defaults if needed
22
+ def init
23
+ # No defaults needed for this class
24
+ # Could add: @company_name ||= 'Unknown Company'
25
+ end
26
+ end
27
+
28
+ class EdgeCaseEmployee < Familia::Horreum
29
+ feature :relationships
30
+
31
+ identifier_field :emp_id
32
+ field :emp_id
33
+ field :email
34
+ field :badge_number
35
+ field :department
36
+ field :status
37
+
38
+ # Class-level unique index for email (auto-populates on save)
39
+ unique_index :email, :email_index
40
+
41
+ # Instance-scoped unique index for badge_number within Company
42
+ unique_index :badge_number, :badge_index, within: EdgeCaseCompany
43
+
44
+ # Multi-index for department (1:many) within Company
45
+ multi_index :department, :dept_index, within: EdgeCaseCompany
46
+
47
+ # init receives no arguments - fields already set from new()
48
+ # Use ||= to apply defaults if needed
49
+ def init
50
+ @status ||= 'active' # Apply default status if not provided
51
+ end
52
+ end
53
+
54
+ class EdgeCaseProduct < Familia::Horreum
55
+ feature :relationships
56
+
57
+ identifier_field :product_id
58
+ field :product_id
59
+ field :sku
60
+
61
+ # Allow empty strings in unique index
62
+ unique_index :sku, :sku_index
63
+
64
+ # init receives no arguments - fields already set from new()
65
+ # Use ||= to apply defaults if needed
66
+ def init
67
+ # No defaults needed for this class
68
+ end
69
+ end
70
+
71
+ # Clear all indexes before starting
72
+ EdgeCaseEmployee.email_index.clear
73
+ EdgeCaseProduct.sku_index.clear
74
+
75
+ # ========================================
76
+ # Test 1: Duplicate instance-scoped index values
77
+ # ========================================
78
+
79
+ ## Setup companies and employees
80
+ @company1 = EdgeCaseCompany.new(company_id: 'c1', company_name: 'Acme Corp')
81
+ @company2 = EdgeCaseCompany.new(company_id: 'c2', company_name: 'Tech Inc')
82
+ @company1.save
83
+ @company2.save
84
+ #=> true
85
+
86
+ ## Create employee with badge_number in company1
87
+ @emp1 = EdgeCaseEmployee.new(emp_id: 'e1', email: 'john@test.com', badge_number: 'B12345')
88
+ @emp1.save # This auto-populates email index
89
+ @emp1.add_to_edge_case_company_badge_index(@company1) # Manual for instance-scoped
90
+ @company1.find_by_badge_number('B12345')&.emp_id
91
+ #=> 'e1'
92
+
93
+ ## Create another employee with same badge_number - guard should detect duplicate
94
+ @emp2 = EdgeCaseEmployee.new(emp_id: 'e2', email: 'jane@test.com', badge_number: 'B12345')
95
+ @emp2.save # Different email is OK
96
+ begin
97
+ @emp2.guard_unique_edge_case_company_badge_index!(@company1)
98
+ false
99
+ rescue Familia::RecordExistsError
100
+ true
101
+ end
102
+ #=> true
103
+
104
+ ## Same badge_number should work in different company (different scope)
105
+ @emp2.add_to_edge_case_company_badge_index(@company2)
106
+ @company2.find_by_badge_number('B12345')&.emp_id
107
+ #=> 'e2'
108
+
109
+ ## Verify badge exists in both companies with different employees
110
+ [@company1.find_by_badge_number('B12345')&.emp_id, @company2.find_by_badge_number('B12345')&.emp_id]
111
+ #=> ['e1', 'e2']
112
+
113
+ ## Cleanup for next test
114
+ EdgeCaseEmployee.email_index.clear
115
+ @company1.badge_index.clear
116
+ @company2.badge_index.clear
117
+ #=> 1
118
+
119
+ # ========================================
120
+ # Test 2: Field updates with auto-index cleanup
121
+ # ========================================
122
+
123
+ ## Create employee with email (save auto-populates index)
124
+ @emp3 = EdgeCaseEmployee.new(emp_id: 'e3', email: 'original@test.com')
125
+ @emp3.save
126
+ EdgeCaseEmployee.find_by_email('original@test.com')&.emp_id
127
+ #=> 'e3'
128
+
129
+ ## Update email - must manually update index (no automatic cleanup on field change)
130
+ old_email = @emp3.email
131
+ @emp3.email = 'updated@test.com'
132
+ @emp3.update_in_class_email_index(old_email) # Manual update required
133
+ EdgeCaseEmployee.find_by_email('original@test.com')
134
+ #=> nil
135
+
136
+ ## New email should resolve to employee
137
+ EdgeCaseEmployee.find_by_email('updated@test.com')&.emp_id
138
+ #=> 'e3'
139
+
140
+ ## Update instance-scoped index
141
+ @emp3.badge_number = 'B99999'
142
+ @emp3.add_to_edge_case_company_badge_index(@company1)
143
+ @company1.find_by_badge_number('B99999')&.emp_id
144
+ #=> 'e3'
145
+
146
+ ## Change badge and update index
147
+ old_badge = @emp3.badge_number
148
+ @emp3.badge_number = 'B11111'
149
+ @emp3.update_in_edge_case_company_badge_index(@company1, old_badge)
150
+ @company1.find_by_badge_number('B99999')
151
+ #=> nil
152
+
153
+ ## New badge should work
154
+ @company1.find_by_badge_number('B11111')&.emp_id
155
+ #=> 'e3'
156
+
157
+ ## Cleanup
158
+ EdgeCaseEmployee.email_index.clear
159
+ @company1.badge_index.clear
160
+ #=> 1
161
+
162
+ # ========================================
163
+ # Test 3: Save within explicit transactions (validation bypass)
164
+ # ========================================
165
+
166
+ ## Create first employee successfully
167
+ @emp4 = EdgeCaseEmployee.new(emp_id: 'e4', email: 'txn@test.com')
168
+ @emp4.save
169
+ EdgeCaseEmployee.find_by_email('txn@test.com')&.emp_id
170
+ #=> 'e4'
171
+
172
+ ## Save cannot be called inside transaction - it raises OperationModeError
173
+ @emp5 = EdgeCaseEmployee.new(emp_id: 'e5', email: 'txn@test.com')
174
+ error_raised = false
175
+ begin
176
+ EdgeCaseEmployee.transaction do |tx|
177
+ @emp5.save # This will raise
178
+ end
179
+ rescue Familia::OperationModeError => e
180
+ error_raised = e.message.include?("Cannot call save within a transaction")
181
+ end
182
+ error_raised
183
+ #=> true
184
+
185
+ ## However, we can bypass validation by manually adding to index inside transaction
186
+ result = EdgeCaseEmployee.transaction do |tx|
187
+ # Manually add without validation (dangerous!)
188
+ EdgeCaseEmployee.email_index['txn_bypass@test.com'] = 'e5'
189
+ 'manual_bypass'
190
+ end
191
+ result.successful?
192
+ #=> true
193
+
194
+ ## After transaction, the manual entry exists (no validation occurred)
195
+ EdgeCaseEmployee.email_index['txn_bypass@test.com']
196
+ #=> 'e5'
197
+
198
+ ## After transaction, the manual entry exists (no validation occurred)
199
+ EdgeCaseEmployee.email_index['txn_bypass@test.com']
200
+ #=> 'e5'
201
+
202
+ ## Cleanup
203
+ EdgeCaseEmployee.email_index.clear
204
+ #=> 1
205
+
206
+ # ========================================
207
+ # Test 4: Multiple empty string values in same index
208
+ # ========================================
209
+
210
+ ## Create product with empty SKU
211
+ @prod1 = EdgeCaseProduct.new(product_id: 'p1', sku: '')
212
+ @prod1.save
213
+ EdgeCaseProduct.find_by_sku('')&.product_id
214
+ #=> 'p1'
215
+
216
+ ## Try to create another product with empty SKU - should fail
217
+ @prod2 = EdgeCaseProduct.new(product_id: 'p2', sku: '')
218
+ begin
219
+ @prod2.save
220
+ false
221
+ rescue Familia::RecordExistsError => e
222
+ e.message.include?('sku=')
223
+ end
224
+ #=> true
225
+
226
+ ## nil values should be skipped (not indexed)
227
+ @prod3 = EdgeCaseProduct.new(product_id: 'p3', sku: nil)
228
+ @prod3.save # Should succeed - nil values aren't indexed
229
+ @prod3.identifier
230
+ #=> 'p3'
231
+
232
+ ## Verify nil doesn't exist in index (empty string != nil)
233
+ EdgeCaseProduct.sku_index[''] # Empty string key
234
+ #=> 'p1'
235
+
236
+ ## nil is not indexed
237
+ EdgeCaseProduct.sku_index.keys.include?(nil)
238
+ #=> false
239
+
240
+ ## Cleanup
241
+ EdgeCaseProduct.sku_index.clear
242
+ #=> 1
243
+
244
+ # ========================================
245
+ # Test 5: Concurrent saves with same unique value
246
+ # ========================================
247
+
248
+ ## Setup fresh index
249
+ EdgeCaseEmployee.email_index.clear
250
+ #=> 0
251
+
252
+ ## Create two employees with same email (simulating race condition)
253
+ @emp6 = EdgeCaseEmployee.new(emp_id: 'e6', email: 'race@test.com')
254
+ @emp7 = EdgeCaseEmployee.new(emp_id: 'e7', email: 'race@test.com')
255
+ @emp7.emp_id
256
+ #=> 'e7'
257
+
258
+ ## First save succeeds
259
+ @emp6.save
260
+ EdgeCaseEmployee.find_by_email('race@test.com')&.emp_id
261
+ #=> 'e6'
262
+
263
+ ## Second save fails due to validation
264
+ begin
265
+ @emp7.save
266
+ false
267
+ rescue Familia::RecordExistsError
268
+ true
269
+ end
270
+ #=> true
271
+
272
+ ## Simulate race condition: both check validation, then both write
273
+ EdgeCaseEmployee.email_index.clear
274
+ @emp8 = EdgeCaseEmployee.new(emp_id: 'e8', email: 'race2@test.com')
275
+ @emp9 = EdgeCaseEmployee.new(emp_id: 'e9', email: 'race2@test.com')
276
+ [@emp8.emp_id, @emp9.emp_id]
277
+ #=> ['e8', 'e9']
278
+
279
+ ## Both pass validation check (index is empty)
280
+ begin
281
+ @emp8.guard_unique_email_index!
282
+ @emp9.guard_unique_email_index!
283
+ true
284
+ rescue
285
+ false
286
+ end
287
+ #=> true
288
+
289
+ ## Both write to index (last write wins in Redis)
290
+ @emp8.add_to_class_email_index
291
+ @emp9.add_to_class_email_index
292
+ # Verify the index contains the identifier (orphaned entry - wastes space but harmless)
293
+ EdgeCaseEmployee.email_index['race2@test.com']
294
+ #=> 'e9'
295
+
296
+ ## find_by returns nil for orphaned index entries (object never saved)
297
+ # This is correct behavior - orphaned entries degrade gracefully to nil
298
+ EdgeCaseEmployee.find_by_email('race2@test.com')
299
+ #=> nil
300
+
301
+ ## To properly handle concurrent saves, check existence inside transaction
302
+ # Note: Can't read inside MULTI block, so need WATCH/MULTI pattern
303
+ result = nil
304
+ EdgeCaseEmployee.dbclient.watch('edge_case_employee:email_index') do
305
+ if EdgeCaseEmployee.email_index['race3@test.com'].nil?
306
+ EdgeCaseEmployee.transaction do |tx|
307
+ EdgeCaseEmployee.email_index['race3@test.com'] = 'e10'
308
+ result = 'success'
309
+ end
310
+ else
311
+ result = 'duplicate'
312
+ end
313
+ end
314
+ result
315
+ #=> 'success'
316
+
317
+ ## Cleanup
318
+ EdgeCaseEmployee.email_index.clear
319
+ #=> 1
320
+
321
+ # ========================================
322
+ # Edge Case: Update with validation in compound operation
323
+ # ========================================
324
+
325
+ ## Test compound index updates in transaction
326
+ @company3 = EdgeCaseCompany.new(company_id: 'c3', company_name: 'Test Corp')
327
+ @company3.save
328
+ #=> true
329
+
330
+ ## Create employee
331
+ @emp11 = EdgeCaseEmployee.new(emp_id: 'e11', email: 'compound@test.com', badge_number: 'B555')
332
+ @emp11.save
333
+ @emp11.add_to_edge_case_company_badge_index(@company3)
334
+ @emp11.emp_id
335
+ #=> 'e11'
336
+
337
+ ## Update multiple indexed fields atomically
338
+ @emp11 = EdgeCaseEmployee.new(emp_id: 'e11', email: 'compound@test.com', badge_number: 'B555')
339
+ @emp11.save
340
+ @emp11.add_to_edge_case_company_badge_index(@company3)
341
+
342
+ old_email = @emp11.email
343
+ old_badge = @emp11.badge_number
344
+ @emp11.email = 'compound_new@test.com'
345
+ @emp11.badge_number = 'B666'
346
+
347
+ # Update both indexes in single transaction
348
+ result = EdgeCaseEmployee.transaction do |tx|
349
+ @emp11.update_in_class_email_index(old_email)
350
+ @emp11.update_in_edge_case_company_badge_index(@company3, old_badge)
351
+ 'updated'
352
+ end
353
+ result.successful?
354
+ #=> true
355
+
356
+ ## Verify updates succeeded
357
+ [EdgeCaseEmployee.find_by_email('compound_new@test.com')&.emp_id, @company3.find_by_badge_number('B666')&.emp_id]
358
+ #=> ['e11', 'e11']
359
+
360
+ ## Old values should be gone
361
+ [EdgeCaseEmployee.find_by_email('compound@test.com'), @company3.find_by_badge_number('B555')]
362
+ #=> [nil, nil]
363
+
364
+
365
+ # Final cleanup
366
+ EdgeCaseEmployee.email_index.clear
367
+ if @company3&.respond_to?(:badge_index) && @company3.badge_index.respond_to?(:clear)
368
+ @company3.badge_index.clear
369
+ end
370
+
371
+ # Clean up test objects - check if they still exist before destroying
372
+ [@company1, @company2, @company3].compact.each do |obj|
373
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
374
+ end
375
+
376
+ puts "All edge case tests completed"
@@ -0,0 +1,281 @@
1
+ # try/unit/horreum/unique_index_guard_validation_try.rb
2
+
3
+ #
4
+ # Unique index guard validation tests
5
+ # Tests the guard_unique_*! methods for both class-level and instance-scoped indexes
6
+ #
7
+
8
+ require_relative '../../support/helpers/test_helpers'
9
+
10
+ # Test classes for unique index guard validation
11
+ class ::GuardUser < Familia::Horreum
12
+ feature :relationships
13
+
14
+ identifier_field :user_id
15
+ field :user_id
16
+ field :email
17
+ field :username
18
+
19
+ # Class-level unique indexes (auto-validated on save)
20
+ unique_index :email, :email_index
21
+ unique_index :username, :username_index
22
+ end
23
+
24
+ class ::GuardCompany < Familia::Horreum
25
+ feature :relationships
26
+
27
+ identifier_field :company_id
28
+ field :company_id
29
+ field :name
30
+ end
31
+
32
+ class ::GuardEmployee < Familia::Horreum
33
+ feature :relationships
34
+
35
+ identifier_field :emp_id
36
+ field :emp_id
37
+ field :badge_number
38
+ field :email
39
+
40
+ # Instance-scoped unique index (manually validated)
41
+ unique_index :badge_number, :badge_index, within: GuardCompany
42
+
43
+ # Class-level unique index (auto-validated)
44
+ unique_index :email, :email_index
45
+ end
46
+
47
+ # Setup
48
+ @user_id1 = "user_#{rand(1000000)}"
49
+ @user_id2 = "user_#{rand(1000000)}"
50
+ @company_id = "comp_#{rand(1000000)}"
51
+ @emp_id1 = "emp_#{rand(1000000)}"
52
+ @emp_id2 = "emp_#{rand(1000000)}"
53
+
54
+ @company = GuardCompany.new(company_id: @company_id, name: 'Test Corp')
55
+ @company.save
56
+
57
+ # =============================================
58
+ # 1. Class-Level Unique Index Guard Methods
59
+ # =============================================
60
+
61
+ ## Guard method exists for class-level unique index
62
+ @user1 = GuardUser.new(user_id: @user_id1, email: 'test@example.com', username: 'testuser')
63
+ @user1.respond_to?(:guard_unique_email_index!)
64
+ #=> true
65
+
66
+ ## Guard passes when no conflict exists
67
+ @user1.guard_unique_email_index!
68
+ #=> nil
69
+
70
+ ## Save succeeds after guard passes
71
+ @user1.save
72
+ #=> true
73
+
74
+ ## Guard fails when duplicate email exists
75
+ @user2 = GuardUser.new(user_id: @user_id2, email: 'test@example.com', username: 'different')
76
+ begin
77
+ @user2.guard_unique_email_index!
78
+ false
79
+ rescue Familia::RecordExistsError => e
80
+ e.message.include?('GuardUser exists email=test@example.com')
81
+ end
82
+ #=> true
83
+
84
+ ## Save automatically calls guard and raises error
85
+ begin
86
+ @user2.save
87
+ false
88
+ rescue Familia::RecordExistsError
89
+ true
90
+ end
91
+ #=> true
92
+
93
+ ## Guard allows same identifier (updating existing record)
94
+ @user1_copy = GuardUser.new(user_id: @user_id1, email: 'test@example.com', username: 'testuser')
95
+ @user1_copy.guard_unique_email_index!
96
+ #=> nil
97
+
98
+ ## Guard handles nil field values gracefully
99
+ @user_nil = GuardUser.new(user_id: "user_nil_#{rand(1000000)}", email: nil, username: 'niluser')
100
+ @user_nil.guard_unique_email_index!
101
+ #=> nil
102
+
103
+ ## Guard handles empty string field values
104
+ @user_empty1 = GuardUser.new(user_id: "user_empty1_#{rand(1000000)}", email: '', username: 'empty1')
105
+ @user_empty1.save
106
+ @user_empty2 = GuardUser.new(user_id: "user_empty2_#{rand(1000000)}", email: '', username: 'empty2')
107
+ begin
108
+ @user_empty2.save
109
+ false
110
+ rescue Familia::RecordExistsError => e
111
+ e.message.include?('GuardUser exists email=')
112
+ end
113
+ #=> true
114
+
115
+ # =============================================
116
+ # 2. Instance-Scoped Unique Index Guard Methods
117
+ # =============================================
118
+
119
+ ## Guard method exists for instance-scoped unique index
120
+ @emp1 = GuardEmployee.new(emp_id: @emp_id1, badge_number: 'BADGE123', email: 'emp1@example.com')
121
+ @emp1.respond_to?(:guard_unique_guard_company_badge_index!)
122
+ #=> true
123
+
124
+ ## Guard method requires parent instance parameter
125
+ @emp1.method(:guard_unique_guard_company_badge_index!).arity
126
+ #=> 1
127
+
128
+ ## Guard passes when no conflict exists in parent's index
129
+ @emp1.guard_unique_guard_company_badge_index!(@company)
130
+ #=> nil
131
+
132
+ ## Can add to index after guard passes
133
+ @emp1.add_to_guard_company_badge_index(@company)
134
+ @company.badge_index.has_key?('BADGE123')
135
+ #=> true
136
+
137
+ ## Guard fails when duplicate badge exists in same company
138
+ @emp2 = GuardEmployee.new(emp_id: @emp_id2, badge_number: 'BADGE123', email: 'emp2@example.com')
139
+ begin
140
+ @emp2.guard_unique_guard_company_badge_index!(@company)
141
+ false
142
+ rescue Familia::RecordExistsError => e
143
+ e.message.include?('GuardEmployee exists in GuardCompany with badge_number=BADGE123')
144
+ end
145
+ #=> true
146
+
147
+ ## Guard allows same employee to re-add (idempotent)
148
+ @emp1.guard_unique_guard_company_badge_index!(@company)
149
+ #=> nil
150
+
151
+ ## Guard passes for different company (different scope)
152
+ @company2_id = "comp_#{rand(1000000)}"
153
+ @company2 = GuardCompany.new(company_id: @company2_id, name: 'Other Corp')
154
+ @company2.save
155
+ @emp2.guard_unique_guard_company_badge_index!(@company2)
156
+ #=> nil
157
+
158
+ ## Can add same badge to different company
159
+ @emp2.add_to_guard_company_badge_index(@company2)
160
+ @company2.badge_index.has_key?('BADGE123')
161
+ #=> true
162
+
163
+ ## Guard handles nil parent instance gracefully
164
+ @emp3 = GuardEmployee.new(emp_id: "emp_#{rand(1000000)}", badge_number: 'BADGE456', email: 'emp3@example.com')
165
+ @emp3.guard_unique_guard_company_badge_index!(nil)
166
+ #=> nil
167
+
168
+ ## Guard handles nil badge_number gracefully
169
+ @emp_nil = GuardEmployee.new(emp_id: "emp_nil_#{rand(1000000)}", badge_number: nil, email: 'empnil@example.com')
170
+ @emp_nil.guard_unique_guard_company_badge_index!(@company)
171
+ #=> nil
172
+
173
+ # =============================================
174
+ # 3. Mixed Class and Instance-Scoped Validation
175
+ # =============================================
176
+
177
+ ## Employee has both class-level and instance-scoped indexes
178
+ @emp4_id = "emp_#{rand(1000000)}"
179
+ @emp4 = GuardEmployee.new(emp_id: @emp4_id, badge_number: 'BADGE789', email: 'unique@example.com')
180
+ @emp4.class
181
+ #=> GuardEmployee
182
+
183
+ ## Class-level email index auto-validates on save
184
+ @emp4.save
185
+ GuardEmployee.find_by_email('unique@example.com')&.emp_id
186
+ #=> @emp4_id
187
+
188
+ ## Instance-scoped badge index must be manually validated and added
189
+ @emp4.guard_unique_guard_company_badge_index!(@company)
190
+ @emp4.add_to_guard_company_badge_index(@company)
191
+ @company.badge_index.has_key?('BADGE789')
192
+ #=> true
193
+
194
+ ## Duplicate class-level index caught by save
195
+ @emp5_id = "emp_#{rand(1000000)}"
196
+ @emp5 = GuardEmployee.new(emp_id: @emp5_id, badge_number: 'BADGE999', email: 'unique@example.com')
197
+ begin
198
+ @emp5.save
199
+ false
200
+ rescue Familia::RecordExistsError => e
201
+ e.message.include?('GuardEmployee exists email=unique@example.com')
202
+ end
203
+ #=> true
204
+
205
+ ## Duplicate instance-scoped index requires manual guard
206
+ @emp6_id = "emp_#{rand(1000000)}"
207
+ @emp6 = GuardEmployee.new(emp_id: @emp6_id, badge_number: 'BADGE789', email: 'emp6@example.com')
208
+ @emp6.save # Succeeds - no auto-validation of instance-scoped indexes
209
+ begin
210
+ @emp6.guard_unique_guard_company_badge_index!(@company)
211
+ false
212
+ rescue Familia::RecordExistsError => e
213
+ e.message.include?('GuardEmployee exists in GuardCompany with badge_number=BADGE789')
214
+ end
215
+ #=> true
216
+
217
+ # =============================================
218
+ # 4. Guard Method Error Messages
219
+ # =============================================
220
+
221
+ ## Class-level guard error includes class and field
222
+ @user_dup = GuardUser.new(user_id: "user_dup_#{rand(1000000)}", email: 'test@example.com', username: 'dupuser')
223
+ begin
224
+ @user_dup.guard_unique_email_index!
225
+ rescue Familia::RecordExistsError => e
226
+ [e.message.include?('GuardUser'), e.message.include?('email=test@example.com')]
227
+ end
228
+ #=> [true, true]
229
+
230
+ ## Instance-scoped guard error includes both classes and field
231
+ begin
232
+ @emp2.guard_unique_guard_company_badge_index!(@company)
233
+ rescue Familia::RecordExistsError => e
234
+ [e.message.include?('GuardEmployee'), e.message.include?('GuardCompany'), e.message.include?('badge_number=BADGE123')]
235
+ end
236
+ #=> [true, true, true]
237
+
238
+ ## RecordExistsError is correct type
239
+ begin
240
+ @emp2.guard_unique_guard_company_badge_index!(@company)
241
+ rescue => e
242
+ e.class
243
+ end
244
+ #=> Familia::RecordExistsError
245
+
246
+ # =============================================
247
+ # 5. Transaction Context Behavior
248
+ # =============================================
249
+
250
+ ## Guard works outside transaction
251
+ @user_tx = GuardUser.new(user_id: "user_tx_#{rand(1000000)}", email: 'tx@example.com', username: 'txuser')
252
+ @user_tx.guard_unique_email_index!
253
+ #=> nil
254
+
255
+ ## Guard must be called outside transaction (new rule)
256
+ unique_timestamp = Time.now.to_i
257
+ unique_rand = rand(1000000)
258
+ email = "tx_unique_#{unique_timestamp}_#{unique_rand}@example.com"
259
+ @user_tx_unique = GuardUser.new(user_id: "user_tx_unique_#{unique_rand}", email: email, username: "txuser_#{unique_rand}")
260
+
261
+ # Guards should be called outside transactions
262
+ @user_tx_unique.send(:guard_unique_indexes!)
263
+ #=> nil
264
+
265
+ # Teardown - clean up test objects
266
+ [@user1, @user2, @user_nil, @user_empty1, @user_empty2, @user_dup, @user_tx, @user_tx_unique].each do |obj|
267
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
268
+ end
269
+
270
+ [@emp1, @emp2, @emp3, @emp_nil, @emp4, @emp5, @emp6].each do |obj|
271
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
272
+ end
273
+
274
+ [@company, @company2].each do |obj|
275
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
276
+ end
277
+
278
+ # Clean up class-level indexes
279
+ [GuardUser.email_index, GuardUser.username_index, GuardEmployee.email_index].each do |index|
280
+ index.delete! if index.respond_to?(:delete!) && index.respond_to?(:exists?) && index.exists?
281
+ end
@@ -1,6 +1,6 @@
1
1
  # try/refinements/dear_json_array_methods_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  class TestArrayWithDearJson < Array
6
6
  include Familia::Refinements::DearJsonArrayMethods
@@ -1,6 +1,6 @@
1
1
  # try/refinements/dear_json_hash_methods_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  class TestHashWithDearJson < Hash
6
6
  include Familia::Refinements::DearJsonHashMethods
@@ -1,6 +1,6 @@
1
1
  # try/refinements/time_literals_numeric_methods_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  class TestNumericWithTimeLiterals
6
6
  include Familia::Refinements::TimeLiterals::NumericMethods
@@ -1,6 +1,6 @@
1
1
  # try/refinements/time_literals_string_methods_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  class TestStringWithTimeLiterals < String
6
6
  include Familia::Refinements::TimeLiterals::StringMethods