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
@@ -1,6 +1,6 @@
1
1
  # try/data_types/unsortedset_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  @a = Bone.new 'atoken'
6
6
 
@@ -5,7 +5,7 @@
5
5
  # Tests automatic index population when Familia::Horreum objects are saved
6
6
  #
7
7
 
8
- require_relative '../helpers/test_helpers'
8
+ require_relative '../../support/helpers/test_helpers'
9
9
 
10
10
  # Test classes for auto-indexing functionality
11
11
  class ::AutoIndexUser < Familia::Horreum
@@ -43,6 +43,19 @@ class ::AutoIndexEmployee < Familia::Horreum
43
43
  multi_index :department, :dept_index, within: AutoIndexCompany
44
44
  end
45
45
 
46
+ class ::AutoIndexWithTransient < Familia::Horreum
47
+ feature :transient_fields
48
+ feature :relationships
49
+
50
+ identifier_field :id
51
+ field :id
52
+ field :email
53
+ transient_field :temp_value
54
+
55
+ unique_index :email, :email_index
56
+ end
57
+
58
+
46
59
  # Setup
47
60
  @user_id = "user_#{rand(1000000)}"
48
61
  @user = AutoIndexUser.new(user_id: @user_id, email: 'test@example.com', username: 'testuser', department: 'engineering')
@@ -125,9 +138,9 @@ AutoIndexUser.email_index.has_key?('')
125
138
  AutoIndexUser.email_index.has_key?('')
126
139
  #=> true
127
140
 
128
- ## Auto-indexing works with create method
141
+ ## Auto-indexing works with create! method
129
142
  @user2_id = "user_#{rand(1000000)}"
130
- @user2 = AutoIndexUser.create(user_id: @user2_id, email: 'create@example.com', username: 'createuser', department: 'marketing')
143
+ @user2 = AutoIndexUser.create!(user_id: @user2_id, email: 'create@example.com', username: 'createuser', department: 'marketing')
131
144
  AutoIndexUser.find_by_email('create@example.com')&.user_id
132
145
  #=> @user2_id
133
146
 
@@ -152,22 +165,11 @@ old_email = @user2.email
152
165
  # =============================================
153
166
 
154
167
  ## Auto-indexing works with transient fields
155
- class ::AutoIndexWithTransient < Familia::Horreum
156
- feature :transient_fields
157
- feature :relationships
158
-
159
- identifier_field :id
160
- field :id
161
- field :email
162
- transient_field :temp_value
163
-
164
- unique_index :email, :email_index
165
- end
166
-
167
168
  @transient_id = "trans_#{rand(1000000)}"
168
- @transient_obj = AutoIndexWithTransient.new(id: @transient_id, email: 'transient@example.com', temp_value: 'ignored')
169
+ @transient_obj = AutoIndexWithTransient.new(id: @transient_id, email: "transient_#{rand(1000000)}@example.com", temp_value: 'ignored')
169
170
  @transient_obj.save
170
- AutoIndexWithTransient.find_by_email('transient@example.com')&.id
171
+ @transient_email = @transient_obj.email
172
+ AutoIndexWithTransient.find_by_email(@transient_email)&.id
171
173
  #=> @transient_id
172
174
 
173
175
  ## Auto-indexing works regardless of other features
@@ -175,6 +177,20 @@ AutoIndexWithTransient.find_by_email('transient@example.com')&.id
175
177
  @transient_obj.class.respond_to?(:indexing_relationships)
176
178
  #=> true
177
179
 
180
+ ## Guard validation prevents duplicate transient field email
181
+ @transient_dup = AutoIndexWithTransient.new(id: "trans_dup_#{rand(1000000)}", email: @transient_email, temp_value: 'duplicate')
182
+ begin
183
+ @transient_dup.save
184
+ rescue Familia::RecordExistsError => ex
185
+ Familia.ld ex.backtrace.join("\n")
186
+ ex.message
187
+ end
188
+ #=:> String
189
+ #=~> /Key already exists/
190
+ #=~> /AutoIndexWithTransient exists /
191
+ #=~> /email=#{@transient_email}/
192
+ #=/=> @transient_email.nil?
193
+
178
194
  # =============================================
179
195
  # 5. Performance and Behavior Verification
180
196
  # =============================================
@@ -0,0 +1,253 @@
1
+ # try/unit/horreum/automatic_index_validation_try.rb
2
+
3
+ #
4
+ # Automatic index validation tests
5
+ # Tests that unique index validation happens automatically when adding to indexes
6
+ #
7
+
8
+ require_relative '../../support/helpers/test_helpers'
9
+
10
+ # Test classes for automatic validation
11
+ class ::AutoValidCompany < Familia::Horreum
12
+ feature :relationships
13
+
14
+ identifier_field :company_id
15
+ field :company_id
16
+ field :name
17
+ end
18
+
19
+ class ::AutoValidEmployee < Familia::Horreum
20
+ feature :relationships
21
+
22
+ identifier_field :emp_id
23
+ field :emp_id
24
+ field :badge_number
25
+ field :email
26
+
27
+ # Instance-scoped unique index (should auto-validate in add_to_* methods)
28
+ unique_index :badge_number, :badge_index, within: AutoValidCompany
29
+
30
+ # Class-level unique index (auto-validates in save)
31
+ unique_index :email, :email_index
32
+ end
33
+
34
+ class ::AutoValidUser < Familia::Horreum
35
+ feature :relationships
36
+
37
+ identifier_field :user_id
38
+ field :user_id
39
+ field :email
40
+
41
+ unique_index :email, :email_index
42
+ end
43
+
44
+ # Setup
45
+ @company_id = "comp_#{rand(1000000)}"
46
+ @company = AutoValidCompany.new(company_id: @company_id, name: 'Test Corp')
47
+ @company.save
48
+
49
+ @emp1_id = "emp_#{rand(1000000)}"
50
+ @emp2_id = "emp_#{rand(1000000)}"
51
+
52
+ # =============================================
53
+ # 1. Automatic Validation in add_to_* Methods
54
+ # =============================================
55
+
56
+ ## First employee can add badge to company index
57
+ @emp1 = AutoValidEmployee.new(emp_id: @emp1_id, badge_number: 'BADGE123', email: 'emp1@example.com')
58
+ @emp1.save # Save first to establish class-level email index
59
+ @emp1.add_to_auto_valid_company_badge_index(@company)
60
+ @company.badge_index.has_key?('BADGE123')
61
+ #=> true
62
+
63
+ ## Duplicate badge is automatically rejected without manual guard call
64
+ @emp2 = AutoValidEmployee.new(emp_id: @emp2_id, badge_number: 'BADGE123', email: 'emp2@example.com')
65
+ begin
66
+ @emp2.add_to_auto_valid_company_badge_index(@company)
67
+ false
68
+ rescue Familia::RecordExistsError => e
69
+ e.message.include?('AutoValidEmployee exists in AutoValidCompany with badge_number=BADGE123')
70
+ end
71
+ #=> true
72
+
73
+ ## Badge was not added after validation failure
74
+ @company.badge_index.get('BADGE123')
75
+ #=> @emp1_id
76
+
77
+ ## Different badge number works fine
78
+ @emp2.badge_number = 'BADGE456'
79
+ @emp2.add_to_auto_valid_company_badge_index(@company)
80
+ @company.badge_index.has_key?('BADGE456')
81
+ #=> true
82
+
83
+ ## Same employee can re-add (idempotent)
84
+ @emp1.add_to_auto_valid_company_badge_index(@company)
85
+ @company.badge_index.get('BADGE123')
86
+ #=> @emp1_id
87
+
88
+ ## Different company allows same badge (scoped uniqueness)
89
+ @company2_id = "comp_#{rand(1000000)}"
90
+ @company2 = AutoValidCompany.new(company_id: @company2_id, name: 'Other Corp')
91
+ @company2.save
92
+ @emp3_id = "emp_#{rand(1000000)}"
93
+ @emp3 = AutoValidEmployee.new(emp_id: @emp3_id, badge_number: 'BADGE123', email: 'emp3@example.com')
94
+ @emp3.add_to_auto_valid_company_badge_index(@company2)
95
+ @company2.badge_index.has_key?('BADGE123')
96
+ #=> true
97
+
98
+ ## Nil badge_number handled gracefully (no validation or addition)
99
+ @emp_nil = AutoValidEmployee.new(emp_id: "emp_nil_#{rand(1000000)}", badge_number: nil, email: 'empnil@example.com')
100
+ @emp_nil.add_to_auto_valid_company_badge_index(@company)
101
+ #=> nil
102
+
103
+ ## Nil parent handled gracefully (no validation or addition)
104
+ @emp4 = AutoValidEmployee.new(emp_id: "emp4_#{rand(1000000)}", badge_number: 'BADGE789', email: 'emp4@example.com')
105
+ @emp4.add_to_auto_valid_company_badge_index(nil)
106
+ #=> nil
107
+
108
+ # =============================================
109
+ # 2. Transaction Detection in save()
110
+ # =============================================
111
+
112
+ ## Normal save works outside transaction
113
+ @user1_id = "user_#{rand(1000000)}"
114
+ @user1 = AutoValidUser.new(user_id: @user1_id, email: 'user1@example.com')
115
+ @user1.save
116
+ #=> true
117
+
118
+ ## save() raises error when called within transaction
119
+ @user2_id = "user_#{rand(1000000)}"
120
+ begin
121
+ AutoValidUser.transaction do
122
+ @user2 = AutoValidUser.new(user_id: @user2_id, email: 'user2@example.com')
123
+ @user2.save
124
+ end
125
+ false
126
+ rescue Familia::OperationModeError => e
127
+ e.message.include?('Cannot call save within a transaction')
128
+ end
129
+ #=> true
130
+
131
+ ## Object was not saved due to transaction error
132
+ AutoValidUser.find_by_email('user2@example.com')
133
+ #=> nil
134
+
135
+ ## Transaction with explicit field updates works (bypass save)
136
+ @user3_id = "user_#{rand(1000000)}"
137
+ @user3 = AutoValidUser.new(user_id: @user3_id, email: 'user3@example.com')
138
+ AutoValidUser.transaction do |_tx|
139
+ @user3.hmset(@user3.to_h_for_storage)
140
+ end
141
+ @user3.exists?
142
+ #=> true
143
+
144
+ ## save() works after transaction completes
145
+ @user4_id = "user_#{rand(1000000)}"
146
+ AutoValidUser.transaction do
147
+ # Do something else in transaction
148
+ end
149
+ @user4 = AutoValidUser.new(user_id: @user4_id, email: 'user4@example.com')
150
+ @user4.save
151
+ #=> true
152
+
153
+ # =============================================
154
+ # 3. Combined Automatic Validation Scenarios
155
+ # =============================================
156
+
157
+ ## Employee with duplicate class-level email caught in save
158
+ @emp5_id = "emp_#{rand(1000000)}"
159
+ @emp5 = AutoValidEmployee.new(emp_id: @emp5_id, badge_number: 'BADGE999', email: 'emp1@example.com')
160
+ begin
161
+ @emp5.save
162
+ false
163
+ rescue Familia::RecordExistsError => e
164
+ e.message.include?('AutoValidEmployee exists email=emp1@example.com')
165
+ end
166
+ #=> true
167
+
168
+ ## Employee can save with unique email
169
+ @emp1.save
170
+ AutoValidEmployee.find_by_email('emp1@example.com')&.emp_id
171
+ #=> @emp1_id
172
+
173
+ ## After save, duplicate instance-scoped index still caught automatically
174
+ @emp6_id = "emp_#{rand(1000000)}"
175
+ @emp6 = AutoValidEmployee.new(emp_id: @emp6_id, badge_number: 'BADGE123', email: 'emp6@example.com')
176
+ @emp6.save # Class-level index is fine
177
+ begin
178
+ @emp6.add_to_auto_valid_company_badge_index(@company) # Instance-scoped duplicate
179
+ false
180
+ rescue Familia::RecordExistsError => e
181
+ e.message.include?('badge_number=BADGE123')
182
+ end
183
+ #=> true
184
+
185
+ # =============================================
186
+ # 4. Error Message Quality
187
+ # =============================================
188
+
189
+ ## Instance-scoped validation error includes both class names
190
+ begin
191
+ @emp2.badge_number = 'BADGE123' # Reset to duplicate
192
+ @emp2.add_to_auto_valid_company_badge_index(@company)
193
+ rescue Familia::RecordExistsError => e
194
+ [e.message.include?('AutoValidEmployee'), e.message.include?('AutoValidCompany')]
195
+ end
196
+ #=> [true, true]
197
+
198
+ ## Instance-scoped validation error includes field name and value
199
+ begin
200
+ @emp2.add_to_auto_valid_company_badge_index(@company)
201
+ rescue Familia::RecordExistsError => e
202
+ [e.message.include?('badge_number'), e.message.include?('BADGE123')]
203
+ end
204
+ #=> [true, true]
205
+
206
+ ## Error type is RecordExistsError
207
+ begin
208
+ @emp2.add_to_auto_valid_company_badge_index(@company)
209
+ rescue => e
210
+ e.class
211
+ end
212
+ #=> Familia::RecordExistsError
213
+
214
+ # =============================================
215
+ # 5. Performance - No Double Validation
216
+ # =============================================
217
+
218
+ ## Manual guard call before add_to_* is redundant but harmless
219
+ @emp7_id = "emp_#{rand(1000000)}"
220
+ @emp7 = AutoValidEmployee.new(emp_id: @emp7_id, badge_number: 'BADGE777', email: 'emp7@example.com')
221
+ @emp7.guard_unique_auto_valid_company_badge_index!(@company)
222
+ @emp7.add_to_auto_valid_company_badge_index(@company)
223
+ @company.badge_index.has_key?('BADGE777')
224
+ #=> true
225
+
226
+ ## Manual guard call detects duplicate
227
+ @emp8_id = "emp_#{rand(1000000)}"
228
+ @emp8 = AutoValidEmployee.new(emp_id: @emp8_id, badge_number: 'BADGE777', email: 'emp8@example.com')
229
+ begin
230
+ @emp8.guard_unique_auto_valid_company_badge_index!(@company) # Should fail - duplicate badge
231
+ false
232
+ rescue Familia::RecordExistsError
233
+ true
234
+ end
235
+ #=> true
236
+
237
+ # Teardown - clean up test objects
238
+ [@emp1, @emp2, @emp3, @emp_nil, @emp4, @emp5, @emp6, @emp7, @emp8].compact.each do |obj|
239
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
240
+ end
241
+
242
+ [@user1, @user3, @user4].compact.each do |obj|
243
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
244
+ end
245
+
246
+ [@company, @company2].each do |obj|
247
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
248
+ end
249
+
250
+ # Clean up class-level indexes
251
+ [AutoValidEmployee.email_index, AutoValidUser.email_index].each do |index|
252
+ index.delete! if index.respond_to?(:delete!) && index.respond_to?(:exists?) && index.exists?
253
+ end
@@ -1,6 +1,6 @@
1
1
  # try/horreum/base_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
 
@@ -41,7 +41,7 @@ Familia.debug = false
41
41
 
42
42
  ## Remove the key
43
43
  @hashkey.delete!
44
- #=> true
44
+ #=> 1
45
45
 
46
46
  ## Horreum objects can update and save their fields (1 of 2)
47
47
  @customer.name = 'John Doe'
@@ -203,11 +203,11 @@ end
203
203
  @aliased.display_size! 100
204
204
  #=> true
205
205
 
206
- ## Aliased field refresh works correctly
206
+ ## Aliased field refresh works correctly (type preserved)
207
207
  @aliased.width = 50 # unsaved change
208
208
  @aliased.refresh!
209
209
  @aliased.width
210
- #=> "100"
210
+ #=> 100
211
211
 
212
212
  ## Fast method with custom name
213
213
  class CustomFastMethodTest < Familia::Horreum
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Test Horreum class methods
4
4
 
5
- require_relative '../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
 
7
7
  TestUser = Class.new(Familia::Horreum) do
8
8
  identifier_field :email
@@ -16,9 +16,9 @@ module AnotherModuleName
16
16
  end
17
17
  end
18
18
 
19
- ## create factory method with existence checking
19
+ ## create! factory method with existence checking
20
20
  TestUser
21
- #==> _.respond_to?(:create)
21
+ #==> _.respond_to?(:create!)
22
22
  #==> _.respond_to?(:exists?)
23
23
 
24
24
  ## multiget method is available
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Test Horreum Valkey/Redis commands
4
4
 
5
- require_relative '../helpers/test_helpers'
5
+ require_relative '../../support/helpers/test_helpers'
6
6
 
7
7
  ## hget/hset operations
8
8
  begin
@@ -1,6 +1,6 @@
1
1
  # try/horreum/defensive_initialization_try.rb
2
2
 
3
- require_relative '../helpers/test_helpers'
3
+ require_relative '../../support/helpers/test_helpers'
4
4
 
5
5
  # Test defensive initialization behavior
6
6
  class User < Familia::Horreum
@@ -9,7 +9,7 @@
9
9
  # This addresses the bug where destroy! only deleted the main object key
10
10
  # but left related field keys in the database.
11
11
 
12
- require_relative '../helpers/test_helpers'
12
+ require_relative '../../support/helpers/test_helpers'
13
13
 
14
14
  MANY_FIELD_MULTIPLIER = 10
15
15
 
@@ -1,6 +1,6 @@
1
1
  # try/horreum/enhanced_conflict_handling_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,44 +1,53 @@
1
1
  # try/horreum/field_categories_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
 
7
- # Define test class with various field categories
7
+ # Define test class with various field types
8
8
  class FieldCategoryTest < Familia::Horreum
9
+ feature :encrypted_fields
10
+ feature :transient_fields
11
+
9
12
  identifier_field :id
10
13
  field :id
11
- field :name # default category (:field)
12
- field :email, category: :encrypted # encrypted category
13
- field :tryouts_cache_data, category: :transient # transient category
14
- field :description, category: :persistent # explicit persistent category
15
- field :settings, category: nil # nil category (defaults to :field)
14
+ field :name # regular field
15
+ encrypted_field :email # encrypted field
16
+ transient_field :tryouts_cache_data # transient field
17
+ field :description # regular persistent field
18
+ field :settings # regular field
16
19
  end
17
20
 
18
21
  # Test class with multiple transient fields
19
22
  class MultiTransientTest < Familia::Horreum
23
+ feature :transient_fields
24
+
20
25
  identifier_field :id
21
26
  field :id
22
27
  field :permanent_data
23
- field :temp1, category: :transient
24
- field :temp2, category: :transient
25
- field :temp3, category: :transient
28
+ transient_field :temp1
29
+ transient_field :temp2
30
+ transient_field :temp3
26
31
  end
27
32
 
28
- # Field categories work with field aliasing
33
+ # Field types work with field aliasing
29
34
  class AliasedCategoryTest < Familia::Horreum
35
+ feature :transient_fields
36
+
30
37
  identifier_field :id
31
38
  field :id
32
- field :internal_temp, as: :temp, category: :transient
33
- field :internal_perm, as: :perm, category: :persistent
39
+ transient_field :internal_temp, as: :temp
40
+ field :internal_perm, as: :perm
34
41
  end
35
42
 
36
43
  # Test edge case with all transient fields
37
44
  class AllTransientTest < Familia::Horreum
45
+ feature :transient_fields
46
+
38
47
  identifier_field :id
39
48
  field :id
40
- field :temp1, category: :transient
41
- field :temp2, category: :transient
49
+ transient_field :temp1
50
+ transient_field :temp2
42
51
  end
43
52
 
44
53
  ## Field types are stored correctly
@@ -58,11 +67,11 @@ FieldCategoryTest.field_types[:email].category
58
67
  FieldCategoryTest.field_types[:tryouts_cache_data].category
59
68
  #=> :transient
60
69
 
61
- ## Explicit persistent category field has correct category
70
+ ## Regular fields have :field category
62
71
  FieldCategoryTest.field_types[:description].category
63
- #=> :persistent
72
+ #=> :field
64
73
 
65
- ## Nil category field defaults to :field
74
+ ## Regular fields default to :field category
66
75
  FieldCategoryTest.field_types[:settings].category
67
76
  #=> :field
68
77
 
@@ -1,6 +1,6 @@
1
1
  # try/horreum/field_definition_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/horreum/initialization_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,11 +97,11 @@ Familia.debug = false
97
97
  @complex.save
98
98
  @complex.refresh!
99
99
  [@complex.custid, @complex.name, @complex.role, @complex.verified]
100
- #=> ["complex@test.com", "Complex User", "admin", "true"]
100
+ #=> ["complex@test.com", "Complex User", "admin", true]
101
101
 
102
102
  ## Clean up saved test objects
103
103
  [@customer6, @complex].map(&:delete!)
104
- #=> [true, true]
104
+ #=> [1, 1]
105
105
 
106
106
  ## "Cleaning up" test objects that were never saved returns true regardless
107
107
  ## b/c it takes place in a transaction and it's the transaction's success